diff --git a/apps/sim/app/api/auth/[...all]/route.test.ts b/apps/sim/app/api/auth/[...all]/route.test.ts index 0d82eac149d..4c577409990 100644 --- a/apps/sim/app/api/auth/[...all]/route.test.ts +++ b/apps/sim/app/api/auth/[...all]/route.test.ts @@ -1,8 +1,8 @@ /** * @vitest-environment node */ -import { createMockRequest } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockRequest, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const handlerMocks = vi.hoisted(() => ({ betterAuthGET: vi.fn(), @@ -12,7 +12,6 @@ const handlerMocks = vi.hoisted(() => ({ user: { id: 'anon' }, session: { id: 'anon-session' }, })), - isAuthDisabled: false, })) vi.mock('better-auth/next-js', () => ({ @@ -31,22 +30,18 @@ vi.mock('@/lib/auth/anonymous', () => ({ createAnonymousSession: handlerMocks.createAnonymousSession, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isAuthDisabled() { - return handlerMocks.isAuthDisabled - }, -})) - import { GET, POST } from '@/app/api/auth/[...all]/route' +afterAll(resetEnvFlagsMock) + describe('auth catch-all route (DISABLE_AUTH get-session)', () => { beforeEach(() => { vi.clearAllMocks() - handlerMocks.isAuthDisabled = false + setEnvFlags({ isAuthDisabled: false }) }) it('returns anonymous session in better-auth response envelope when auth is disabled', async () => { - handlerMocks.isAuthDisabled = true + setEnvFlags({ isAuthDisabled: true }) const req = createMockRequest( 'GET', @@ -67,7 +62,7 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => { }) it('delegates to better-auth handler when auth is enabled', async () => { - handlerMocks.isAuthDisabled = false + setEnvFlags({ isAuthDisabled: false }) const { NextResponse } = await import('next/server') handlerMocks.betterAuthGET.mockResolvedValueOnce( diff --git a/apps/sim/app/api/billing/switch-plan/route.test.ts b/apps/sim/app/api/billing/switch-plan/route.test.ts index 778020298d4..377907a9f8e 100644 --- a/apps/sim/app/api/billing/switch-plan/route.test.ts +++ b/apps/sim/app/api/billing/switch-plan/route.test.ts @@ -1,8 +1,8 @@ /** * @vitest-environment node */ -import { createMockRequest } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockRequest, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockCanManageWorkspaceBilling, @@ -48,10 +48,6 @@ vi.mock('@/lib/billing/workspace-permissions', () => ({ canManageWorkspaceBilling: mockCanManageWorkspaceBilling, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn(), })) @@ -62,6 +58,12 @@ vi.mock('@/lib/workspaces/host-context', () => ({ import { POST } from '@/app/api/billing/switch-plan/route' +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('POST /api/billing/switch-plan', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/app/api/billing/update-cost/route.test.ts b/apps/sim/app/api/billing/update-cost/route.test.ts index df2a53a61d1..2edb84d2a31 100644 --- a/apps/sim/app/api/billing/update-cost/route.test.ts +++ b/apps/sim/app/api/billing/update-cost/route.test.ts @@ -1,8 +1,8 @@ /** * @vitest-environment node */ -import { createMockRequest } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockRequest, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockCheckInternalApiKey, @@ -15,7 +15,6 @@ const { mockToBillingContext, MockCumulativeUsageContextMismatchError, MockThresholdSettlementError, - billingState, } = vi.hoisted(() => ({ mockCheckInternalApiKey: vi.fn(), mockRecordCumulativeUsage: vi.fn(), @@ -36,10 +35,6 @@ const { this.code = code } }, - billingState: { - isBillingEnabled: true, - isCopilotBillingProtocolRequired: false, - }, })) vi.mock('@/lib/copilot/request/http', () => ({ @@ -82,18 +77,11 @@ vi.mock('@/lib/billing/threshold-billing', () => ({ ThresholdSettlementError: MockThresholdSettlementError, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return billingState.isBillingEnabled - }, - get isCopilotBillingProtocolRequired() { - return billingState.isCopilotBillingProtocolRequired - }, -})) - import { billingUpdateCostBodySchema } from '@/lib/api/contracts/subscription' import { POST } from '@/app/api/billing/update-cost/route' +afterAll(resetEnvFlagsMock) + const ACCOUNT_BILLING_DECISION = { userId: 'user-1', billingEntity: { type: 'organization' as const, id: 'account-org' }, @@ -159,8 +147,7 @@ const KEYLESS_UPDATE_COST_BODY = { describe('POST /api/billing/update-cost — workspaceId attribution', () => { beforeEach(() => { vi.clearAllMocks() - billingState.isBillingEnabled = true - billingState.isCopilotBillingProtocolRequired = false + setEnvFlags({ isBillingEnabled: true, isCopilotBillingProtocolRequired: false }) mockCheckInternalApiKey.mockReturnValue({ success: true }) mockRecordCumulativeUsage.mockResolvedValue({ billed: true, delta: 0.5, total: 0.5 }) mockCheckAndBillOverageThreshold.mockResolvedValue(undefined) @@ -178,7 +165,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) it('returns 401 for a billing-disabled request without valid internal auth', async () => { - billingState.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockCheckInternalApiKey.mockReturnValue({ success: false, error: 'Invalid internal API key' }) const res = await POST( @@ -200,7 +187,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) it('returns no-op success for old markerless Go when billing is disabled', async () => { - billingState.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const res = await POST( createMockRequest( @@ -303,7 +290,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) it('rejects markerless callbacks only when protocol-required is explicitly enabled', async () => { - billingState.isCopilotBillingProtocolRequired = true + setEnvFlags({ isCopilotBillingProtocolRequired: true }) const res = await POST( createMockRequest('POST', OLD_GO_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal' }) ) @@ -328,7 +315,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) it('rejects explicitly labeled legacy callbacks without admission attribution', async () => { - billingState.isCopilotBillingProtocolRequired = true + setEnvFlags({ isCopilotBillingProtocolRequired: true }) const res = await POST( createMockRequest('POST', EXPLICIT_LEGACY_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal', @@ -343,7 +330,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) it('bills explicitly labeled legacy callbacks from their admission attribution', async () => { - billingState.isCopilotBillingProtocolRequired = true + setEnvFlags({ isCopilotBillingProtocolRequired: true }) const res = await POST( createMockRequest('POST', EXPLICIT_LEGACY_HOSTED_UPDATE_COST_BODY, { 'x-api-key': 'internal', diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index bc765862b4c..f883fdaf6c3 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -11,6 +11,8 @@ import { encryptionMock, encryptionMockFns, resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, workflowsApiUtilsMock, workflowsApiUtilsMockFns, workflowsOrchestrationMock, @@ -18,7 +20,7 @@ import { workflowsPersistenceUtilsMock, } from '@sim/testing' import { NextRequest } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockCheckChatAccess, mockValidateChatDeployAuth } = vi.hoisted(() => ({ mockCheckChatAccess: vi.fn(), @@ -37,11 +39,6 @@ const mockNotifySocketDeploymentChanged = workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged vi.mock('@sim/audit', () => auditMock) -vi.mock('@/lib/core/config/env-flags', () => ({ - isDev: true, - isHosted: false, - isProd: false, -})) vi.mock('@sim/db', () => dbChainMock) vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) vi.mock('@/lib/core/security/encryption', () => encryptionMock) @@ -66,6 +63,12 @@ vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock) import { DELETE, GET, PATCH } from '@/app/api/chat/manage/[id]/route' import { ChatDeployAuthNotAllowedError } from '@/ee/access-control/utils/permission-check' +beforeAll(() => { + setEnvFlags({ isDev: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('Chat Edit API Route', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/app/api/copilot/api-keys/validate/route.test.ts b/apps/sim/app/api/copilot/api-keys/validate/route.test.ts index 7e8b86e5a87..d0faa279153 100644 --- a/apps/sim/app/api/copilot/api-keys/validate/route.test.ts +++ b/apps/sim/app/api/copilot/api-keys/validate/route.test.ts @@ -1,7 +1,14 @@ /** * @vitest-environment node */ -import { createMockRequest, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { + createMockRequest, + queueTableRows, + resetDbChainMock, + resetEnvFlagsMock, + schemaMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -18,7 +25,6 @@ const { mockSerializeBillingAttributionHeader, mockGetUserEntityPermissions, mockGetWorkspaceBillingSettings, - mockFlags, } = vi.hoisted(() => ({ mockCheckInternalApiKey: vi.fn(), mockCheckAttributedUsageLimits: vi.fn(), @@ -33,9 +39,6 @@ const { mockSerializeBillingAttributionHeader: vi.fn(), mockGetUserEntityPermissions: vi.fn(), mockGetWorkspaceBillingSettings: vi.fn(), - mockFlags: { - isCopilotBillingProtocolRequired: false, - }, })) const ATTRIBUTION = { @@ -119,12 +122,6 @@ vi.mock('@/lib/copilot/request/otel', () => ({ ) => fn({ setAttribute: vi.fn(), setAttributes: vi.fn() }), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isCopilotBillingProtocolRequired() { - return mockFlags.isCopilotBillingProtocolRequired - }, -})) - vi.mock('@/lib/workspaces/permissions/utils', () => ({ getUserEntityPermissions: mockGetUserEntityPermissions, })) @@ -136,6 +133,8 @@ vi.mock('@/lib/workspaces/utils', () => ({ import { validateCopilotApiKeyBodySchema } from '@/lib/api/contracts/copilot' import { POST } from '@/app/api/copilot/api-keys/validate/route' +afterAll(resetEnvFlagsMock) + function request(body: Record, headers: Record = {}) { return createMockRequest('POST', body, { 'x-api-key': 'internal', ...headers }) } @@ -144,7 +143,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isCopilotBillingProtocolRequired = false + setEnvFlags({ isCopilotBillingProtocolRequired: false }) mockCheckInternalApiKey.mockReturnValue({ success: true }) queueTableRows(schemaMock.user, [{ id: 'user-1' }]) mockResolveBillingAttribution.mockResolvedValue(ATTRIBUTION) @@ -258,7 +257,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => { }) it('rejects markerless admission only when protocol-required is explicitly enabled', async () => { - mockFlags.isCopilotBillingProtocolRequired = true + setEnvFlags({ isCopilotBillingProtocolRequired: true }) const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY)) expect(res.status).toBe(400) @@ -267,7 +266,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => { }) it('allows explicitly labeled legacy requests when markerless traffic is disabled', async () => { - mockFlags.isCopilotBillingProtocolRequired = true + setEnvFlags({ isCopilotBillingProtocolRequired: true }) const res = await POST( request(OLD_GO_HOSTED_VALIDATE_BODY, { 'x-sim-billing-protocol': 'legacy-v0' }) ) diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 012b6f9f458..4b5c2194584 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -7,10 +7,11 @@ import { createMockRequest, envFlagsMock, hybridAuthMockFns, + resetEnvFlagsMock, workflowsUtilsMock, } from '@sim/testing' import { NextRequest } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockExecuteInE2B, @@ -101,14 +102,14 @@ vi.mock('@/lib/uploads', () => ({ vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - import { validateProxyUrl } from '@/lib/core/security/input-validation' import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache' import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata' import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref' import { POST } from '@/app/api/function/execute/route' +afterAll(resetEnvFlagsMock) + describe('Function Execute API Route', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/app/api/knowledge/search/utils.test.ts b/apps/sim/app/api/knowledge/search/utils.test.ts index 35535fa913a..0c0d4128d09 100644 --- a/apps/sim/app/api/knowledge/search/utils.test.ts +++ b/apps/sim/app/api/knowledge/search/utils.test.ts @@ -9,8 +9,6 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites import { env } from '@/lib/core/config/env' import * as documentsUtilsModule from '@/lib/knowledge/documents/utils' -vi.mock('drizzle-orm') - /** * 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, diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts index 753fb41b249..8d8428f78d1 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts @@ -1,8 +1,14 @@ /** * @vitest-environment node */ -import { auditMock, createMockRequest, createSession } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + auditMock, + createMockRequest, + createSession, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetSession, @@ -11,7 +17,6 @@ const { mockGetOrgMemberUsageForCurrentPeriod, mockSetOrgMemberUsageLimit, mockGetOrganizationSubscription, - mockFlags, } = vi.hoisted(() => ({ mockGetSession: vi.fn(), mockIsOrganizationOwnerOrAdmin: vi.fn(), @@ -19,7 +24,6 @@ const { mockGetOrgMemberUsageForCurrentPeriod: vi.fn(), mockSetOrgMemberUsageLimit: vi.fn(), mockGetOrganizationSubscription: vi.fn(), - mockFlags: { isHosted: true }, })) vi.mock('@/lib/auth', () => ({ @@ -43,14 +47,10 @@ vi.mock('@/lib/billing/core/billing', () => ({ getOrganizationSubscription: mockGetOrganizationSubscription, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return mockFlags.isHosted - }, -})) - import { GET, PUT } from '@/app/api/organizations/[id]/members/[memberId]/usage-limit/route' +afterAll(resetEnvFlagsMock) + function context() { return { params: Promise.resolve({ id: 'org-1', memberId: 'user-2' }) } } @@ -66,7 +66,7 @@ function getRequest() { describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { beforeEach(() => { vi.clearAllMocks() - mockFlags.isHosted = true + setEnvFlags({ isHosted: true }) mockGetSession.mockResolvedValue(createSession({ userId: 'admin-1' })) mockIsOrganizationOwnerOrAdmin.mockResolvedValue(true) mockGetOrgMemberUsageForCurrentPeriod.mockResolvedValue(1) // $1 -> 200 credits @@ -81,7 +81,7 @@ describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { }) it('returns 404 when not hosted', async () => { - mockFlags.isHosted = false + setEnvFlags({ isHosted: false }) const res = await GET(getRequest(), context()) expect(res.status).toBe(404) }) @@ -144,14 +144,14 @@ describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { describe('PUT /api/organizations/[id]/members/[memberId]/usage-limit', () => { beforeEach(() => { vi.clearAllMocks() - mockFlags.isHosted = true + setEnvFlags({ isHosted: true }) mockGetSession.mockResolvedValue(createSession({ userId: 'admin-1' })) mockIsOrganizationOwnerOrAdmin.mockResolvedValue(true) mockSetOrgMemberUsageLimit.mockResolvedValue(undefined) }) it('returns 404 when not hosted', async () => { - mockFlags.isHosted = false + setEnvFlags({ isHosted: false }) const res = await PUT(putRequest({ creditLimit: 400 }), context()) expect(res.status).toBe(404) expect(mockSetOrgMemberUsageLimit).not.toHaveBeenCalled() diff --git a/apps/sim/app/api/organizations/[id]/session-policy/route.test.ts b/apps/sim/app/api/organizations/[id]/session-policy/route.test.ts index 5530045fd50..0e1d39adaaa 100644 --- a/apps/sim/app/api/organizations/[id]/session-policy/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/session-policy/route.test.ts @@ -8,8 +8,10 @@ import { dbChainMockFns, queueTableRows, resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetSession, mockIsEnterprise, mockEagerClamp, mockRecordAudit } = vi.hoisted(() => ({ mockGetSession: vi.fn(), @@ -37,10 +39,6 @@ vi.mock('@/lib/billing/core/subscription', () => ({ isOrganizationOnEnterprisePlan: mockIsEnterprise, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - vi.mock('@sim/audit', () => ({ recordAudit: mockRecordAudit, AuditAction: { @@ -54,6 +52,12 @@ import { GET, PUT } from '@/app/api/organizations/[id]/session-policy/route' const ORG_ID = 'org-1' const routeContext = { params: Promise.resolve({ id: ORG_ID }) } +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('session policy route', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/app/api/schedules/execute/route.test.ts b/apps/sim/app/api/schedules/execute/route.test.ts index bcf0eaf5d3f..4de1d5aaebf 100644 --- a/apps/sim/app/api/schedules/execute/route.test.ts +++ b/apps/sim/app/api/schedules/execute/route.test.ts @@ -3,9 +3,16 @@ * * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, requestUtilsMockFns, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + requestUtilsMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { type NextRequest, NextResponse } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const orderByLimitMock = vi.fn() @@ -14,7 +21,6 @@ const { mockExecuteScheduleJob, mockExecuteJobInline, mockReleaseScheduleLock, - mockFeatureFlags, mockEnqueue, mockGetJob, mockStartJob, @@ -29,12 +35,6 @@ const { mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined), mockExecuteJobInline: vi.fn().mockResolvedValue(undefined), mockReleaseScheduleLock: vi.fn().mockResolvedValue(undefined), - mockFeatureFlags: { - isTriggerDevEnabled: false, - isHosted: false, - isProd: false, - isDev: true, - }, mockEnqueue: vi.fn().mockResolvedValue('job-id-1'), mockGetJob: vi.fn().mockResolvedValue(null), mockStartJob: vi.fn().mockResolvedValue(undefined), @@ -70,8 +70,6 @@ vi.mock('@/background/schedule-execution', () => ({ }), })) -vi.mock('@/lib/core/config/env-flags', () => mockFeatureFlags) - vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn().mockResolvedValue({ enqueue: mockEnqueue, @@ -277,6 +275,8 @@ function createMockRequest(): NextRequest { } as NextRequest } +afterAll(resetEnvFlagsMock) + describe('Scheduled Workflow Execution API Route', () => { beforeEach(() => { vi.clearAllMocks() @@ -289,10 +289,7 @@ describe('Scheduled Workflow Execution API Route', () => { dbChainMockFns.orderBy.mockReturnValue({ limit: orderByLimitMock } as never) dbChainMockFns.execute.mockResolvedValue([{ acquired: true }] as never) requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('test-request-id') - mockFeatureFlags.isTriggerDevEnabled = false - mockFeatureFlags.isHosted = false - mockFeatureFlags.isProd = false - mockFeatureFlags.isDev = true + setEnvFlags({ isTriggerDevEnabled: false, isHosted: false, isProd: false, isDev: true }) mockShouldExecuteInline.mockReturnValue(false) mockEnqueue.mockReset() mockEnqueue.mockResolvedValue('job-id-1') @@ -337,7 +334,7 @@ describe('Scheduled Workflow Execution API Route', () => { }) it('should queue schedules to Trigger.dev when enabled', async () => { - mockFeatureFlags.isTriggerDevEnabled = true + setEnvFlags({ isTriggerDevEnabled: true }) dbChainMockFns.limit .mockResolvedValueOnce(SINGLE_CLAIMED_SCHEDULE_ROWS) .mockResolvedValueOnce([]) diff --git a/apps/sim/app/api/speech/token/route.test.ts b/apps/sim/app/api/speech/token/route.test.ts index 5f42626ba41..ab3fd86ed23 100644 --- a/apps/sim/app/api/speech/token/route.test.ts +++ b/apps/sim/app/api/speech/token/route.test.ts @@ -72,11 +72,6 @@ vi.mock('@/app/api/workflows/utils', () => ({ vi.mock('@/lib/core/config/env', () => ({ env: { ELEVENLABS_API_KEY: 'test-key' } })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: false, - getCostMultiplier: () => 1, -})) - vi.mock('@/lib/core/rate-limiter', () => ({ RateLimiter: class { checkRateLimitDirect = vi.fn().mockResolvedValue({ allowed: true }) diff --git a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts index 5ca3fb8c04d..38b24e06fb0 100644 --- a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts @@ -1,9 +1,9 @@ /** * @vitest-environment node */ -import { hybridAuthMockFns } from '@sim/testing' +import { hybridAuthMockFns, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' import { NextRequest, NextResponse } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { TableDefinition } from '@/lib/table' const { @@ -13,7 +13,6 @@ const { mockRunTableDelete, mockTableFilterError, mockTasksTrigger, - flags, } = vi.hoisted(() => ({ mockCheckAccess: vi.fn(), mockMarkTableJobRunning: vi.fn(), @@ -21,7 +20,6 @@ const { mockRunTableDelete: vi.fn(), mockTableFilterError: vi.fn(), mockTasksTrigger: vi.fn(), - flags: { triggerDev: false }, })) vi.mock('@sim/utils/id', () => ({ @@ -33,11 +31,6 @@ vi.mock('@/lib/table/jobs/service', () => ({ releaseJobClaim: mockReleaseJobClaim, })) vi.mock('@/lib/table/delete-runner', () => ({ runTableDelete: mockRunTableDelete })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isTriggerDevEnabled() { - return flags.triggerDev - }, -})) vi.mock('@/background/table-delete', () => ({ tableDeleteTask: { id: 'table-delete' } })) vi.mock('@/lib/core/async-jobs/region', () => ({ resolveTriggerRegion: vi.fn().mockResolvedValue('us-east-1'), @@ -63,6 +56,8 @@ vi.mock('@/app/api/table/utils', async () => { import { POST } from '@/app/api/table/[tableId]/delete-async/route' +afterAll(resetEnvFlagsMock) + function buildTable(overrides: Partial = {}): TableDefinition { return { id: 'tbl_1', @@ -109,7 +104,7 @@ describe('POST /api/table/[tableId]/delete-async', () => { mockRunTableDelete.mockResolvedValue(undefined) mockTableFilterError.mockReturnValue(null) mockTasksTrigger.mockResolvedValue({ id: 'run_1' }) - flags.triggerDev = false + setEnvFlags({ isTriggerDevEnabled: false }) }) it('claims the job slot and kicks off the delete worker with filter + exclusions', async () => { @@ -185,7 +180,7 @@ describe('POST /api/table/[tableId]/delete-async', () => { }) it('routes through trigger.dev (ISO cutoff, tagged) when the flag is on', async () => { - flags.triggerDev = true + setEnvFlags({ isTriggerDevEnabled: true }) const response = await makeRequest(validBody) expect(response.status).toBe(200) @@ -204,7 +199,7 @@ describe('POST /api/table/[tableId]/delete-async', () => { }) it('releases the job claim when the trigger.dev dispatch fails (no ghost running job)', async () => { - flags.triggerDev = true + setEnvFlags({ isTriggerDevEnabled: true }) mockTasksTrigger.mockRejectedValueOnce(new Error('trigger.dev unreachable')) const response = await makeRequest(validBody) diff --git a/apps/sim/app/api/tools/onepassword/utils.test.ts b/apps/sim/app/api/tools/onepassword/utils.test.ts index 8d9e9a76672..160bbe69b7f 100644 --- a/apps/sim/app/api/tools/onepassword/utils.test.ts +++ b/apps/sim/app/api/tools/onepassword/utils.test.ts @@ -1,17 +1,11 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDnsLookup, hostedFlag } = vi.hoisted(() => ({ +const { mockDnsLookup } = vi.hoisted(() => ({ mockDnsLookup: vi.fn(), - hostedFlag: { value: false }, -})) - -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return hostedFlag.value - }, })) vi.mock('dns/promises', () => ({ @@ -20,10 +14,12 @@ vi.mock('dns/promises', () => ({ import { validateConnectServerUrl } from '@/app/api/tools/onepassword/utils' +afterAll(resetEnvFlagsMock) + describe('validateConnectServerUrl', () => { beforeEach(() => { vi.clearAllMocks() - hostedFlag.value = false + setEnvFlags({ isHosted: false }) }) it('rejects a non-URL string', async () => { @@ -32,7 +28,7 @@ describe('validateConnectServerUrl', () => { describe('hosted deployment', () => { beforeEach(() => { - hostedFlag.value = true + setEnvFlags({ isHosted: true }) }) it.each([ @@ -87,7 +83,7 @@ describe('validateConnectServerUrl', () => { describe('self-hosted deployment', () => { beforeEach(() => { - hostedFlag.value = false + setEnvFlags({ isHosted: false }) }) it.each([ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.test.tsx index 6fdcac86f98..6e84fab43ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.test.tsx @@ -2,8 +2,9 @@ * @vitest-environment jsdom */ import { act } from 'react' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockHostContext, mockPush, mockSession, mockUseWorkspaceCreditAvailability } = vi.hoisted( () => ({ @@ -55,10 +56,6 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: () => ({ data: mockSession.current }), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - vi.mock('@/app/workspace/[workspaceId]/providers/workspace-host-provider', () => ({ useWorkspaceHostContext: () => mockHostContext.current, })) @@ -76,6 +73,12 @@ import { CreditsChip } from '@/app/workspace/[workspaceId]/home/components/credi let container: HTMLDivElement let root: Root +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('CreditsChip', () => { beforeEach(() => { container = document.createElement('div') diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.test.tsx index cbf1d3c3cd0..c1f8bf34dec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.test.tsx @@ -2,8 +2,9 @@ * @vitest-environment jsdom */ import { act, type ReactNode } from 'react' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { hostContext, mockUseOrganizationBilling } = vi.hoisted(() => ({ hostContext: { @@ -32,10 +33,6 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: () => ({ data: { user: { email: 'viewer@example.com' } } }), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - vi.mock('@/app/workspace/[workspaceId]/providers/workspace-host-provider', () => ({ useWorkspaceHostContext: () => hostContext.current, })) @@ -66,6 +63,12 @@ import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/ let container: HTMLDivElement let root: Root +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('InviteModal organization billing isolation', () => { beforeEach(() => { container = document.createElement('div') diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index 3dc571b9dd7..bf90c25d656 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -1,13 +1,10 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockIsHosted, mockIsAzureConfigured, mockIsOllamaConfigured } = vi.hoisted(() => ({ - mockIsHosted: { value: false }, - mockIsAzureConfigured: { value: false }, - mockIsOllamaConfigured: { value: false }, -})) +afterAll(resetEnvFlagsMock) const { mockGetHostedModels, @@ -34,18 +31,6 @@ const { mockProviders } = vi.hoisted(() => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return mockIsHosted.value - }, - get isAzureConfigured() { - return mockIsAzureConfigured.value - }, - get isOllamaConfigured() { - return mockIsOllamaConfigured.value - }, -})) - vi.mock('@/providers/models', () => ({ getProviderFileAttachment: vi .fn() @@ -103,9 +88,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { beforeEach(() => { vi.clearAllMocks() - mockIsHosted.value = false - mockIsAzureConfigured.value = false - mockIsOllamaConfigured.value = false + setEnvFlags({ isHosted: false, isAzureConfigured: false, isOllamaConfigured: false }) mockProviders.value = { base: { models: [], isLoading: false }, ollama: { models: [], isLoading: false }, @@ -131,14 +114,14 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { describe('hosted models', () => { it('does not require API key for hosted models on hosted platform', () => { - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) mockGetHostedModels.mockReturnValue(['gpt-4o', 'claude-sonnet-4-5']) expect(evaluateCondition('gpt-4o')).toBe(false) expect(evaluateCondition('claude-sonnet-4-5')).toBe(false) }) it('requires API key for non-hosted models on hosted platform', () => { - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) mockGetHostedModels.mockReturnValue(['gpt-4o']) expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) }) @@ -158,14 +141,14 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { describe('Azure models', () => { it('does not require API key for azure/ models when Azure is configured', () => { - mockIsAzureConfigured.value = true + setEnvFlags({ isAzureConfigured: true }) expect(evaluateCondition('azure/gpt-4o')).toBe(false) expect(evaluateCondition('azure-openai/gpt-4o')).toBe(false) expect(evaluateCondition('azure-anthropic/claude-sonnet-4-5')).toBe(false) }) it('requires API key for azure/ models when Azure is not configured', () => { - mockIsAzureConfigured.value = false + setEnvFlags({ isAzureConfigured: false }) expect(evaluateCondition('azure/gpt-4o')).toBe(true) }) }) @@ -218,7 +201,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { describe('Ollama — OLLAMA_URL env var (server-safe)', () => { it('does not require API key for unknown models when OLLAMA_URL is set', () => { - mockIsOllamaConfigured.value = true + setEnvFlags({ isOllamaConfigured: true }) expect(evaluateCondition('llama3:latest')).toBe(false) expect(evaluateCondition('phi3:latest')).toBe(false) expect(evaluateCondition('gemma2:latest')).toBe(false) @@ -226,7 +209,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) it('does not require API key for Ollama models that match cloud provider regex patterns', () => { - mockIsOllamaConfigured.value = true + setEnvFlags({ isOllamaConfigured: true }) expect(evaluateCondition('mistral:latest')).toBe(false) expect(evaluateCondition('mistral')).toBe(false) expect(evaluateCondition('mistral-nemo')).toBe(false) @@ -234,7 +217,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) it('requires API key for known cloud models even when OLLAMA_URL is set', () => { - mockIsOllamaConfigured.value = true + setEnvFlags({ isOllamaConfigured: true }) mockGetBaseModelProviders.mockReturnValue(BASE_CLOUD_MODELS) expect(evaluateCondition('gpt-4o')).toBe(true) expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) @@ -243,7 +226,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) it('requires API key for slash-prefixed cloud models when OLLAMA_URL is set', () => { - mockIsOllamaConfigured.value = true + setEnvFlags({ isOllamaConfigured: true }) expect(evaluateCondition('azure/gpt-4o')).toBe(true) expect(evaluateCondition('fireworks/llama-3')).toBe(true) expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) @@ -253,7 +236,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { describe('cloud provider models that need API key', () => { it('requires API key for standard cloud models on hosted platform', () => { - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) mockGetHostedModels.mockReturnValue([]) expect(evaluateCondition('gpt-4o')).toBe(true) expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) @@ -262,7 +245,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) it('requires API key for prefixed cloud models on hosted platform', () => { - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) expect(evaluateCondition('fireworks/llama-3')).toBe(true) expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) expect(evaluateCondition('groq/llama-3')).toBe(true) @@ -270,7 +253,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) it('requires API key for prefixed cloud models on self-hosted', () => { - mockIsHosted.value = false + setEnvFlags({ isHosted: false }) expect(evaluateCondition('fireworks/llama-3')).toBe(true) expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) expect(evaluateCondition('groq/llama-3')).toBe(true) @@ -280,8 +263,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { describe('self-hosted without OLLAMA_URL', () => { it('requires API key for any model (Ollama models cannot appear without OLLAMA_URL)', () => { - mockIsHosted.value = false - mockIsOllamaConfigured.value = false + setEnvFlags({ isHosted: false, isOllamaConfigured: false }) expect(evaluateCondition('llama3:latest')).toBe(true) expect(evaluateCondition('mistral:latest')).toBe(true) expect(evaluateCondition('gpt-4o')).toBe(true) diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index 6ea563bcec9..2aea7cf535c 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -2,12 +2,17 @@ * @vitest-environment node */ import { permissionGroup } from '@sim/db/schema' -import { queueTableRows, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { + envFlagsMockFns, + queueTableRows, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { DEFAULT_PERMISSION_GROUP_CONFIG, - mockGetAllowedIntegrationsFromEnv, mockIsOrganizationOnEnterprisePlan, mockGetWorkspaceWithOwner, mockGetProviderFromModel, @@ -39,7 +44,6 @@ const { hideDeployChatbot: false, allowedChatDeployAuthTypes: null, }, - mockGetAllowedIntegrationsFromEnv: vi.fn<() => string[] | null>(), mockIsOrganizationOnEnterprisePlan: vi.fn<() => Promise>(), mockGetWorkspaceWithOwner: vi.fn<() => Promise<{ organizationId: string | null } | null>>(), mockGetProviderFromModel: vi.fn<(model: string) => string>(), @@ -54,14 +58,6 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceWithOwner: mockGetWorkspaceWithOwner, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - getAllowedIntegrationsFromEnv: mockGetAllowedIntegrationsFromEnv, - isAccessControlEnabled: true, - isHosted: true, - isInvitationsDisabled: false, - isPublicApiDisabled: false, -})) - vi.mock('@/lib/permission-groups/types', () => ({ DEFAULT_PERMISSION_GROUP_CONFIG, parsePermissionGroupConfig: (config: unknown) => { @@ -141,6 +137,14 @@ beforeEach(() => { mockGetBlock.mockImplementation(() => undefined) }) +const mockGetAllowedIntegrationsFromEnv = envFlagsMockFns.getAllowedIntegrationsFromEnv + +beforeAll(() => { + setEnvFlags({ isAccessControlEnabled: true, isHosted: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('IntegrationNotAllowedError', () => { it.concurrent('creates error with correct name and message', () => { const error = new IntegrationNotAllowedError('discord') diff --git a/apps/sim/ee/sso/components/sso-settings.test.tsx b/apps/sim/ee/sso/components/sso-settings.test.tsx index f252bf6cefc..9ce0f36f06b 100644 --- a/apps/sim/ee/sso/components/sso-settings.test.tsx +++ b/apps/sim/ee/sso/components/sso-settings.test.tsx @@ -2,8 +2,9 @@ * @vitest-environment jsdom */ import { act, type ChangeEventHandler, type ReactNode } from 'react' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockUseConfigureSSO, mockUseOrganizationBilling, mockUseSession, mockUseSSOProviders } = vi.hoisted(() => ({ @@ -50,10 +51,6 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: mockUseSession, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - vi.mock( '@/app/workspace/[workspaceId]/settings/components/save-discard-actions/save-discard-actions', () => ({ @@ -130,6 +127,12 @@ function renderSso(organizationId: string) { }) } +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('SSO organization transitions', () => { beforeEach(() => { // The component reads getBaseUrl() during render; make sure the env var is diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index a5a5b36b410..6fc48121454 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -1,5 +1,22 @@ -import { dbChainMock, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' -import { afterAll, afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { + dbChainMock, + queueTableRows, + resetDbChainMock, + resetEnvFlagsMock, + schemaMock, + setEnvFlags, +} from '@sim/testing' +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from 'vitest' import { getAllBlocks } from '@/blocks' import { BlockType, isMcpTool } from '@/executor/constants' import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' @@ -11,19 +28,6 @@ import { executeTool } from '@/tools' process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' -vi.mock('@/lib/core/config/env-flags', () => ({ - isHosted: false, - isProd: false, - isDev: true, - isTest: false, - getCostMultiplier: vi.fn().mockReturnValue(1), - getAllowedIntegrationsFromEnv: vi.fn().mockReturnValue(null), - isEmailVerificationEnabled: false, - isBillingEnabled: false, - isOrganizationsEnabled: false, - isAccessControlEnabled: false, -})) - vi.mock('@/providers/utils', () => ({ getProviderFromModel: vi.fn().mockReturnValue('mock-provider'), transformBlockTool: vi.fn(), @@ -110,6 +114,12 @@ const mockTransformBlockTool = transformBlockTool as Mock const mockFetch = vi.fn() const mockExecuteProviderRequest = executeProviderRequest as Mock +beforeAll(() => { + setEnvFlags({ isDev: true, isTest: false }) +}) + +afterAll(resetEnvFlagsMock) + describe('AgentBlockHandler', () => { let handler: AgentBlockHandler let mockBlock: SerializedBlock diff --git a/apps/sim/executor/handlers/pi/keys.test.ts b/apps/sim/executor/handlers/pi/keys.test.ts index 97d17264f6c..cb1c6bbcde2 100644 --- a/apps/sim/executor/handlers/pi/keys.test.ts +++ b/apps/sim/executor/handlers/pi/keys.test.ts @@ -1,7 +1,8 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { envFlagsMockFns, resetEnvFlagsMock } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetApiKeyWithBYOK, mockGetBYOKKey, mockCalculateCost, mockShouldBill } = vi.hoisted( () => ({ @@ -20,10 +21,15 @@ vi.mock('@/providers/utils', () => ({ calculateCost: mockCalculateCost, shouldBillModelUsage: mockShouldBill, })) -vi.mock('@/lib/core/config/env-flags', () => ({ getCostMultiplier: () => 2 })) import { computePiCost, providerApiKeyEnvVar, resolvePiModelKey } from '@/executor/handlers/pi/keys' +beforeAll(() => { + envFlagsMockFns.getCostMultiplier.mockReturnValue(2) +}) + +afterAll(resetEnvFlagsMock) + describe('providerApiKeyEnvVar', () => { it('maps key-based providers and rejects unsupported ones', () => { expect(providerApiKeyEnvVar('anthropic')).toBe('ANTHROPIC_API_KEY') diff --git a/apps/sim/lib/api-key/byok.test.ts b/apps/sim/lib/api-key/byok.test.ts index aec86b6e6b1..b049ae42738 100644 --- a/apps/sim/lib/api-key/byok.test.ts +++ b/apps/sim/lib/api-key/byok.test.ts @@ -22,10 +22,6 @@ vi.mock('@/lib/core/config/env', () => ({ env: {}, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isHosted: false, -})) - vi.mock('@/providers/models', () => ({ getProviderFileAttachment: vi .fn() diff --git a/apps/sim/lib/auth/access-control.test.ts b/apps/sim/lib/auth/access-control.test.ts index 5667f724ca6..38b50e378e8 100644 --- a/apps/sim/lib/auth/access-control.test.ts +++ b/apps/sim/lib/auth/access-control.test.ts @@ -1,10 +1,11 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { AccessControlConfig } from '@/lib/auth/access-control' -const { mockFetch, envRef, flagRef } = vi.hoisted(() => ({ +const { mockFetch, envRef } = vi.hoisted(() => ({ mockFetch: vi.fn(), envRef: { APPCONFIG_APPLICATION: 'sim-staging' as string | undefined, @@ -15,7 +16,6 @@ const { mockFetch, envRef, flagRef } = vi.hoisted(() => ({ ALLOWED_LOGIN_DOMAINS: undefined as string | undefined, BLOCKED_EMAIL_MX_HOSTS: undefined as string | undefined, }, - flagRef: { isAppConfigEnabled: false }, })) vi.mock('@/lib/core/config/appconfig', () => ({ @@ -28,12 +28,6 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isAppConfigEnabled() { - return flagRef.isAppConfigEnabled - }, -})) - import { getAccessControlConfig, isEmailBlockedByAccessControl, @@ -48,10 +42,12 @@ const empty: AccessControlConfig = { blockedEmailMxHosts: [], } +afterAll(resetEnvFlagsMock) + describe('getAccessControlConfig', () => { beforeEach(() => { vi.clearAllMocks() - flagRef.isAppConfigEnabled = false + setEnvFlags({ isAppConfigEnabled: false }) Object.assign(envRef, { BLOCKED_SIGNUP_DOMAINS: undefined, BLOCKED_EMAILS: undefined, @@ -81,7 +77,7 @@ describe('getAccessControlConfig', () => { describe('AppConfig source (enabled)', () => { beforeEach(() => { - flagRef.isAppConfigEnabled = true + setEnvFlags({ isAppConfigEnabled: true }) }) it('reads the access-control profile and normalizes the payload', async () => { diff --git a/apps/sim/lib/auth/ban.test.ts b/apps/sim/lib/auth/ban.test.ts index fffdfb634ae..a6c81ad4faf 100644 --- a/apps/sim/lib/auth/ban.test.ts +++ b/apps/sim/lib/auth/ban.test.ts @@ -25,7 +25,6 @@ vi.mock('@/lib/core/config/env', () => ({ return envRef }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ isAppConfigEnabled: false })) import { getActivelyBannedUserIds, isBanActive, isEmailBlocked } from '@/lib/auth/ban' diff --git a/apps/sim/lib/billing/calculations/usage-monitor.test.ts b/apps/sim/lib/billing/calculations/usage-monitor.test.ts index b1a498cd455..ddd9b902474 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.test.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.test.ts @@ -1,30 +1,25 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { - mockFlags, mockGetOrgMemberUsageForBillingPeriod, mockGetOrgMemberUsageLimit, mockIsOrganizationBillingBlocked, } = vi.hoisted(() => ({ - mockFlags: { isHosted: true, isBillingEnabled: true }, mockGetOrgMemberUsageForBillingPeriod: vi.fn(), mockGetOrgMemberUsageLimit: vi.fn(), mockIsOrganizationBillingBlocked: vi.fn(), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return mockFlags.isHosted - }, - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) - vi.mock('@sim/db', () => dbChainMock) vi.mock('@/lib/billing/organizations/member-limits', () => ({ @@ -53,12 +48,13 @@ afterAll(() => { resetDbChainMock() }) +afterAll(resetEnvFlagsMock) + describe('checkBillingBlocked', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isHosted = true - mockFlags.isBillingEnabled = true + setEnvFlags({ isHosted: true, isBillingEnabled: true }) dbChainMockFns.limit.mockResolvedValue([{ blocked: false, blockedReason: null }]) }) @@ -76,8 +72,7 @@ describe('checkBillingEntityBlocked', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isHosted = true - mockFlags.isBillingEnabled = true + setEnvFlags({ isHosted: true, isBillingEnabled: true }) mockIsOrganizationBillingBlocked.mockResolvedValue(false) dbChainMockFns.limit.mockResolvedValue([]) }) @@ -116,8 +111,7 @@ describe('checkOrganizationMemberUsageLimit', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isHosted = true - mockFlags.isBillingEnabled = true + setEnvFlags({ isHosted: true, isBillingEnabled: true }) mockGetOrgMemberUsageLimit.mockResolvedValue(2) mockGetOrgMemberUsageForBillingPeriod.mockResolvedValue(1) }) @@ -139,14 +133,14 @@ describe('checkOrganizationMemberUsageLimit', () => { }) it('no-ops when not hosted', async () => { - mockFlags.isHosted = false + setEnvFlags({ isHosted: false }) const result = await checkOrganizationMemberUsageLimit('actor-1', 'org-1', billingPeriod) expect(result.isExceeded).toBe(false) expect(mockGetOrgMemberUsageLimit).not.toHaveBeenCalled() }) it('no-ops when billing is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const result = await checkOrganizationMemberUsageLimit('actor-1', 'org-1', billingPeriod) expect(result.isExceeded).toBe(false) expect(mockGetOrgMemberUsageLimit).not.toHaveBeenCalled() diff --git a/apps/sim/lib/billing/calculations/usage-reservation-env.test.ts b/apps/sim/lib/billing/calculations/usage-reservation-env.test.ts index 9a998355bf7..1a2697253c0 100644 --- a/apps/sim/lib/billing/calculations/usage-reservation-env.test.ts +++ b/apps/sim/lib/billing/calculations/usage-reservation-env.test.ts @@ -1,8 +1,8 @@ /** * @vitest-environment node */ -import { redisConfigMock, redisConfigMockFns } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { redisConfigMock, redisConfigMockFns, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { evalMock, mockEnv } = vi.hoisted(() => ({ evalMock: vi.fn(), @@ -31,15 +31,16 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, - isHosted: true, -})) - vi.mock('@/lib/core/config/redis', () => redisConfigMock) import { reserveExecutionSlot } from '@/lib/billing/calculations/usage-reservation' +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true, isHosted: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('usage reservation environment overrides', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/billing/calculations/usage-reservation.test.ts b/apps/sim/lib/billing/calculations/usage-reservation.test.ts index ea8d6332aec..23615006588 100644 --- a/apps/sim/lib/billing/calculations/usage-reservation.test.ts +++ b/apps/sim/lib/billing/calculations/usage-reservation.test.ts @@ -1,21 +1,8 @@ /** * @vitest-environment node */ -import { redisConfigMock, redisConfigMockFns } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockFlags } = vi.hoisted(() => ({ - mockFlags: { isBillingEnabled: true, isHosted: true }, -})) - -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, - get isHosted() { - return mockFlags.isHosted - }, -})) +import { redisConfigMock, redisConfigMockFns, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/config/redis', () => redisConfigMock) @@ -54,11 +41,13 @@ function hashTag(key: string): string | undefined { return key.match(/\{([^}]+)\}/)?.[1] } +afterAll(resetEnvFlagsMock) + describe('usage-reservation', () => { beforeEach(() => { vi.clearAllMocks() - mockFlags.isBillingEnabled = true - mockFlags.isHosted = true + setEnvFlags({ isBillingEnabled: true }) + setEnvFlags({ isHosted: true }) redisConfigMockFns.mockGetRedisClient.mockReturnValue(fakeRedis) }) @@ -253,14 +242,14 @@ describe('usage-reservation', () => { }) it('is a no-op when billing enforcement is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const result = await reserveExecutionSlot(baseParams) expect(result.reserved).toBe(true) expect(evalMock).not.toHaveBeenCalled() }) it('is a no-op on self-hosted deployments', async () => { - mockFlags.isHosted = false + setEnvFlags({ isHosted: false }) const result = await reserveExecutionSlot(baseParams) expect(result).toEqual({ reserved: true, created: false }) expect(evalMock).not.toHaveBeenCalled() @@ -427,7 +416,7 @@ describe('usage-reservation', () => { }) it('is a no-op when billing enforcement is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) await releaseExecutionSlot('exec-1') expect(getMock).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/billing/cleanup-dispatcher.test.ts b/apps/sim/lib/billing/cleanup-dispatcher.test.ts index ed126c26535..fccdaf8e6c9 100644 --- a/apps/sim/lib/billing/cleanup-dispatcher.test.ts +++ b/apps/sim/lib/billing/cleanup-dispatcher.test.ts @@ -1,11 +1,16 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockFlags, mockIsTriggerAvailable } = vi.hoisted(() => ({ - mockFlags: { isBillingEnabled: false }, +const { mockIsTriggerAvailable } = vi.hoisted(() => ({ mockIsTriggerAvailable: vi.fn(), })) @@ -18,11 +23,6 @@ vi.mock('@/lib/cleanup/batch-delete', () => ({ chunkArray: vi.fn() })) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn() })) vi.mock('@/lib/core/async-jobs/config', () => ({ shouldExecuteInline: vi.fn(() => false) })) vi.mock('@/lib/core/async-jobs/region', () => ({ resolveTriggerRegion: vi.fn() })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) vi.mock('@/lib/knowledge/documents/service', () => ({ isTriggerAvailable: mockIsTriggerAvailable, })) @@ -33,11 +33,13 @@ vi.mock('@/lib/workspaces/policy', () => ({ import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher' +afterAll(resetEnvFlagsMock) + describe('dispatchCleanupJobs billing gate', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) }) afterAll(() => { diff --git a/apps/sim/lib/billing/core/billing-attribution.test.ts b/apps/sim/lib/billing/core/billing-attribution.test.ts index 05f42edf884..588a5a985c2 100644 --- a/apps/sim/lib/billing/core/billing-attribution.test.ts +++ b/apps/sim/lib/billing/core/billing-attribution.test.ts @@ -1,11 +1,16 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { - mockFlags, mockCheckBillingBlocked, mockCheckBillingEntityBlocked, mockCheckOrganizationMemberUsageLimit, @@ -13,7 +18,6 @@ const { mockGetHighestPriorityPersonalSubscription, mockGetOrganizationSubscription, } = vi.hoisted(() => ({ - mockFlags: { isBillingEnabled: true, isHosted: true }, mockCheckBillingBlocked: vi.fn(), mockCheckBillingEntityBlocked: vi.fn(), mockCheckOrganizationMemberUsageLimit: vi.fn(), @@ -22,15 +26,6 @@ const { mockGetOrganizationSubscription: vi.fn(), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, - get isHosted() { - return mockFlags.isHosted - }, -})) - vi.mock('@sim/db', () => dbChainMock) vi.mock('@/lib/billing/calculations/usage-monitor', () => ({ @@ -79,6 +74,8 @@ const ORG_SUBSCRIPTION = { periodEnd: new Date('2026-08-01T00:00:00.000Z'), } +afterAll(resetEnvFlagsMock) + describe('resolveBillingAttribution', () => { beforeEach(() => { vi.clearAllMocks() @@ -480,8 +477,8 @@ describe('checkAttributedUsageLimits', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isBillingEnabled = true - mockFlags.isHosted = true + setEnvFlags({ isBillingEnabled: true }) + setEnvFlags({ isHosted: true }) mockCheckBillingBlocked.mockResolvedValue({ blocked: false }) mockCheckBillingEntityBlocked.mockResolvedValue({ blocked: false }) mockCheckUsageStatus.mockResolvedValue({ @@ -522,7 +519,7 @@ describe('checkAttributedUsageLimits', () => { } it('skips hosted freezes and caps when billing is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) await expect(checkAttributedUsageLimits(attribution)).resolves.toEqual({ isExceeded: false, diff --git a/apps/sim/lib/billing/core/limit-notifications.test.ts b/apps/sim/lib/billing/core/limit-notifications.test.ts index 5c82b418b72..37ce9ac295a 100644 --- a/apps/sim/lib/billing/core/limit-notifications.test.ts +++ b/apps/sim/lib/billing/core/limit-notifications.test.ts @@ -6,33 +6,23 @@ import { dbChainMockFns, queueTableRows, resetDbChainMock, + resetEnvFlagsMock, schemaMock, + setEnvFlags, } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { - billingFlag, - sendEmailSpy, - getEmailPreferencesMock, - renderMock, - subjectMock, - isOrgAdminRoleMock, -} = vi.hoisted(() => ({ - billingFlag: { enabled: true }, - sendEmailSpy: vi.fn(() => Promise.resolve({ success: true })), - getEmailPreferencesMock: vi.fn(() => Promise.resolve(null as unknown)), - renderMock: vi.fn(() => Promise.resolve('')), - subjectMock: vi.fn(() => 'Subject'), - isOrgAdminRoleMock: vi.fn(() => true), -})) +const { sendEmailSpy, getEmailPreferencesMock, renderMock, subjectMock, isOrgAdminRoleMock } = + vi.hoisted(() => ({ + sendEmailSpy: vi.fn(() => Promise.resolve({ success: true })), + getEmailPreferencesMock: vi.fn(() => Promise.resolve(null as unknown)), + renderMock: vi.fn(() => Promise.resolve('')), + subjectMock: vi.fn(() => 'Subject'), + isOrgAdminRoleMock: vi.fn(() => true), + })) vi.mock('@sim/db', () => dbChainMock) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return billingFlag.enabled - }, -})) vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://app.sim.ai' })) vi.mock('@/lib/messaging/email/mailer', () => ({ sendEmail: sendEmailSpy })) vi.mock('@/lib/messaging/email/unsubscribe', () => ({ @@ -57,11 +47,13 @@ const baseUserParams = { userName: 'Ada', } +afterAll(resetEnvFlagsMock) + describe('maybeSendLimitThresholdEmail', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - billingFlag.enabled = true + setEnvFlags({ isBillingEnabled: true }) dbChainMockFns.returning.mockResolvedValue([{ id: 'u1' }]) getEmailPreferencesMock.mockResolvedValue(null) }) @@ -134,7 +126,7 @@ describe('maybeSendLimitThresholdEmail', () => { }) it('skips entirely when billing is disabled', async () => { - billingFlag.enabled = false + setEnvFlags({ isBillingEnabled: false }) await maybeSendLimitThresholdEmail({ ...baseUserParams, currentUsage: 5, limit: 5 }) expect(dbChainMockFns.returning).not.toHaveBeenCalled() expect(sendEmailSpy).not.toHaveBeenCalled() diff --git a/apps/sim/lib/billing/core/subscription.test.ts b/apps/sim/lib/billing/core/subscription.test.ts index f5b2c114c08..af08492f9e4 100644 --- a/apps/sim/lib/billing/core/subscription.test.ts +++ b/apps/sim/lib/billing/core/subscription.test.ts @@ -1,8 +1,8 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, urlsMock } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { dbChainMock, dbChainMockFns, resetEnvFlagsMock, setEnvFlags, urlsMock } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetHighestPrioritySubscription, @@ -55,14 +55,6 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceWithOwner: mockGetWorkspaceWithOwner, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isAccessControlEnabled: false, - isBillingEnabled: true, - isHosted: true, - isInboxEnabled: false, - isSsoEnabled: false, -})) - vi.mock('@/lib/core/utils/urls', () => urlsMock) import { @@ -74,6 +66,12 @@ import { syncSubscriptionPlan, } from '@/lib/billing/core/subscription' +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true, isHosted: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('hasPaidSubscription', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/billing/core/usage-log.test.ts b/apps/sim/lib/billing/core/usage-log.test.ts index 6b1861ed1ac..b03d8ff569b 100644 --- a/apps/sim/lib/billing/core/usage-log.test.ts +++ b/apps/sim/lib/billing/core/usage-log.test.ts @@ -2,8 +2,14 @@ * @vitest-environment node */ import { usageLog } from '@sim/db/schema' -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetHighestPrioritySubscription, @@ -35,10 +41,6 @@ vi.mock('@/lib/billing/subscriptions/utils', () => ({ isOrgScopedSubscription: mockIsOrgScopedSubscription, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isBillingEnabled: true, -})) - import { CUMULATIVE_COST_EPSILON, CumulativeUsageContextMismatchError, @@ -61,6 +63,12 @@ afterAll(() => { resetDbChainMock() }) +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('recordUsage', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/billing/organizations/seat-drift.test.ts b/apps/sim/lib/billing/organizations/seat-drift.test.ts index a95ffa0d2f9..4b36402bdaf 100644 --- a/apps/sim/lib/billing/organizations/seat-drift.test.ts +++ b/apps/sim/lib/billing/organizations/seat-drift.test.ts @@ -1,12 +1,18 @@ /** * @vitest-environment node */ -import { dbChainMock, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { + dbChainMock, + queueTableRows, + resetDbChainMock, + resetEnvFlagsMock, + schemaMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockReconcileOrganizationSeats, mockFeatureFlags } = vi.hoisted(() => ({ +const { mockReconcileOrganizationSeats } = vi.hoisted(() => ({ mockReconcileOrganizationSeats: vi.fn(), - mockFeatureFlags: { isBillingEnabled: true }, })) vi.mock('@sim/db', () => dbChainMock) @@ -15,19 +21,15 @@ vi.mock('@/lib/billing/organizations/seats', () => ({ reconcileOrganizationSeats: mockReconcileOrganizationSeats, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFeatureFlags.isBillingEnabled - }, -})) - import { reconcileTeamSeatDrift } from '@/lib/billing/organizations/seat-drift' +afterAll(resetEnvFlagsMock) + describe('reconcileTeamSeatDrift', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockReconcileOrganizationSeats.mockResolvedValue({ changed: true, previousSeats: 1, seats: 2 }) }) @@ -99,7 +101,7 @@ describe('reconcileTeamSeatDrift', () => { }) it('no-ops when billing is disabled', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const result = await reconcileTeamSeatDrift() diff --git a/apps/sim/lib/billing/organizations/seats.test.ts b/apps/sim/lib/billing/organizations/seats.test.ts index 26154aa1987..fbfac95f4ab 100644 --- a/apps/sim/lib/billing/organizations/seats.test.ts +++ b/apps/sim/lib/billing/organizations/seats.test.ts @@ -7,14 +7,15 @@ import { dbChainMockFns, queueTableRows, resetDbChainMock, + resetEnvFlagsMock, schemaMock, + setEnvFlags, } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockSyncSubscriptionUsageLimits, enqueueMock, mockFeatureFlags } = vi.hoisted(() => ({ +const { mockSyncSubscriptionUsageLimits, enqueueMock } = vi.hoisted(() => ({ mockSyncSubscriptionUsageLimits: vi.fn(), enqueueMock: vi.fn(), - mockFeatureFlags: { isBillingEnabled: true }, })) vi.mock('@sim/db', () => dbChainMock) @@ -33,12 +34,6 @@ vi.mock('@/lib/billing/webhooks/outbox-handlers', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFeatureFlags.isBillingEnabled - }, -})) - vi.mock('@sim/audit', () => auditMock) import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' @@ -57,12 +52,14 @@ function queueReconcileReads(subscriptionRows: unknown[], memberCountRows: unkno queueTableRows(schemaMock.member, memberCountRows) } +afterAll(resetEnvFlagsMock) + describe('reconcileOrganizationSeats', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() enqueueMock.mockResolvedValue('evt-1') - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) }) afterAll(() => { @@ -201,7 +198,7 @@ describe('reconcileOrganizationSeats', () => { }) it('no-ops when billing is disabled', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const result = await reconcileOrganizationSeats({ organizationId: 'org-1', diff --git a/apps/sim/lib/billing/storage/limits.test.ts b/apps/sim/lib/billing/storage/limits.test.ts index c690221cdfb..117dd1d2a58 100644 --- a/apps/sim/lib/billing/storage/limits.test.ts +++ b/apps/sim/lib/billing/storage/limits.test.ts @@ -1,12 +1,17 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockEq, mockFlags, mockGetHighestPrioritySubscription } = vi.hoisted(() => ({ +const { mockEq, mockGetHighestPrioritySubscription } = vi.hoisted(() => ({ mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value })), - mockFlags: { isBillingEnabled: true }, mockGetHighestPrioritySubscription: vi.fn(), })) @@ -39,12 +44,6 @@ vi.mock('@/lib/core/config/env', () => ({ getEnv: mockGetEnv, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) - import type { StorageBillingContext } from '@/lib/billing/storage/context' import { checkStorageQuota, @@ -73,11 +72,13 @@ const USER_CONTEXT: StorageBillingContext = { const GIB = 1024 ** 3 +afterAll(resetEnvFlagsMock) + describe('storage limits and quota', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetEnv.mockReturnValue(undefined) dbChainMockFns.limit.mockResolvedValue([{ storageUsedBytes: 1024 }]) mockGetHighestPrioritySubscription.mockResolvedValue(null) @@ -147,7 +148,7 @@ describe('storage limits and quota', () => { }) it('applies identical disabled-billing behavior without resolving context', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const expected = { allowed: true, @@ -161,7 +162,7 @@ describe('storage limits and quota', () => { }) it('opts into free-tier enforcement when FREE_STORAGE_LIMIT_GB is explicitly set', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetEnv.mockImplementation((variable: string) => variable === 'FREE_STORAGE_LIMIT_GB' ? '1' : undefined ) diff --git a/apps/sim/lib/billing/storage/tracking.test.ts b/apps/sim/lib/billing/storage/tracking.test.ts index 40cbcf664d6..33709cbbcf3 100644 --- a/apps/sim/lib/billing/storage/tracking.test.ts +++ b/apps/sim/lib/billing/storage/tracking.test.ts @@ -1,10 +1,10 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { envFlagsMock, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { - mockFlags, mockGetStorageLimitForBillingContext, mockGetStorageUsageForBillingContext, mockGetUserStorageLimit, @@ -24,7 +24,6 @@ const { mockTxWhere, mockWorkspaceRow, } = vi.hoisted(() => ({ - mockFlags: { isBillingEnabled: true }, mockGetStorageLimitForBillingContext: vi.fn(), mockGetStorageUsageForBillingContext: vi.fn(), mockGetUserStorageLimit: vi.fn(), @@ -92,19 +91,13 @@ vi.mock('@/lib/billing/storage/limits', () => ({ getUserStorageLimit: mockGetUserStorageLimit, getUserStorageUsage: mockGetUserStorageUsage, // No FREE_STORAGE_LIMIT_GB opt-in in these tests, so enforcement === billing. - isStorageEnforcementEnabled: () => mockFlags.isBillingEnabled, + isStorageEnforcementEnabled: () => envFlagsMock.isBillingEnabled, })) vi.mock('@/lib/core/config/env', () => ({ getEnv: vi.fn(() => undefined), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) - vi.mock('@sim/logger', () => ({ createLogger: () => ({ error: mockLoggerError, @@ -130,10 +123,16 @@ const ORG_CONTEXT: StorageBillingContext = { customStorageLimitGB: null, } +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('workspace storage counter mutations', () => { beforeEach(() => { vi.clearAllMocks() - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockWorkspaceRow.current = { billedAccountUserId: 'workspace-owner', organizationId: 'workspace-org', @@ -298,7 +297,7 @@ describe('workspace storage counter mutations', () => { }) it('keeps durable workspace and payer ledgers accurate while billing is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) await incrementStorageUsageForBillingContextInTx(mockTx as unknown as DbOrTx, ORG_CONTEXT, 100) diff --git a/apps/sim/lib/billing/validation/seat-management.test.ts b/apps/sim/lib/billing/validation/seat-management.test.ts index cbab22d9898..61194fca2ed 100644 --- a/apps/sim/lib/billing/validation/seat-management.test.ts +++ b/apps/sim/lib/billing/validation/seat-management.test.ts @@ -1,15 +1,19 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockFeatureFlags, mockGetOrganizationSubscription, mockHasInflightOutboxEvent } = - vi.hoisted(() => ({ - mockFeatureFlags: { isBillingEnabled: false }, - mockGetOrganizationSubscription: vi.fn(), - mockHasInflightOutboxEvent: vi.fn(), - })) +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetOrganizationSubscription, mockHasInflightOutboxEvent } = vi.hoisted(() => ({ + mockGetOrganizationSubscription: vi.fn(), + mockHasInflightOutboxEvent: vi.fn(), +})) vi.mock('@sim/db', () => dbChainMock) @@ -37,12 +41,6 @@ vi.mock('@/lib/billing/subscriptions/utils', () => ({ getEffectiveSeats: vi.fn().mockReturnValue(10), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFeatureFlags.isBillingEnabled - }, -})) - vi.mock('@/lib/messaging/email/validation', () => ({ quickValidateEmail: vi.fn((email: string) => ({ isValid: email.includes('@') })), })) @@ -73,11 +71,13 @@ function queueSelectResponses(responses: unknown[][]) { }) } +afterAll(resetEnvFlagsMock) + describe('getOrganizationSeatInfo', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetOrganizationSubscription.mockResolvedValue(null) }) @@ -103,7 +103,7 @@ describe('validateSeatAvailability', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetOrganizationSubscription.mockResolvedValue({ id: 'sub-1', plan: 'team', diff --git a/apps/sim/lib/copilot/chat/payload.test.ts b/apps/sim/lib/copilot/chat/payload.test.ts index 85aec553ba3..00cdaa0ff19 100644 --- a/apps/sim/lib/copilot/chat/payload.test.ts +++ b/apps/sim/lib/copilot/chat/payload.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { envFlagsMock, workflowsUtilsMock } from '@sim/testing' +import { workflowsUtilsMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockCreateUserToolSchema, mockGetHighestPrioritySubscription } = vi.hoisted(() => ({ @@ -19,8 +19,6 @@ vi.mock('@/lib/billing/plan-helpers', () => ({ ), })) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - vi.mock('@/lib/mcp/utils', () => ({ createMcpToolId: vi.fn(), })) diff --git a/apps/sim/lib/copilot/request/lifecycle/run.test.ts b/apps/sim/lib/copilot/request/lifecycle/run.test.ts index 1fbd1802997..ad2551c7ce8 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.test.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.test.ts @@ -2,7 +2,8 @@ * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { ExecutionContext, StreamingContext } from '@/lib/copilot/request/types' const { @@ -16,7 +17,6 @@ const { mockToolWatchdogTimeoutMs, mockUpdateRunStatus, mockEnv, - mockFlags, } = vi.hoisted(() => ({ mockCreateRunSegment: vi.fn(), mockForceFailHungToolCall: vi.fn(), @@ -30,10 +30,6 @@ const { mockEnv: { COPILOT_API_KEY: undefined as string | undefined, }, - mockFlags: { - isHosted: false, - isCopilotBillingAttributionV1Enabled: false, - }, })) vi.mock('@/lib/copilot/async-runs/repository', () => ({ @@ -81,15 +77,6 @@ vi.mock('@/lib/core/config/env', () => ({ isFalsy: vi.fn((value: string | undefined) => value === 'false'), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isCopilotBillingAttributionV1Enabled() { - return mockFlags.isCopilotBillingAttributionV1Enabled - }, - get isHosted() { - return mockFlags.isHosted - }, -})) - vi.mock('@/lib/environment/utils', () => ({ getEffectiveDecryptedEnv: mockGetEffectiveDecryptedEnv, })) @@ -112,12 +99,14 @@ import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothershi import { CopilotBackendError } from '@/lib/copilot/request/go/stream' import { runCopilotLifecycle } from '@/lib/copilot/request/lifecycle/run' +afterAll(resetEnvFlagsMock) + describe('runCopilotLifecycle', () => { beforeEach(() => { vi.clearAllMocks() mockEnv.COPILOT_API_KEY = undefined - mockFlags.isHosted = false - mockFlags.isCopilotBillingAttributionV1Enabled = false + setEnvFlags({ isHosted: false }) + setEnvFlags({ isCopilotBillingAttributionV1Enabled: false }) mockGetMothershipBaseURL.mockResolvedValue('http://mothership.test') mockGetMothershipSourceEnvHeaders.mockReturnValue({}) }) @@ -381,8 +370,8 @@ describe('runCopilotLifecycle', () => { }, payerSubscription: null, } - mockFlags.isHosted = true - mockFlags.isCopilotBillingAttributionV1Enabled = true + setEnvFlags({ isHosted: true }) + setEnvFlags({ isCopilotBillingAttributionV1Enabled: true }) mockEnv.COPILOT_API_KEY = 'sim-agent-key' mockGetEffectiveDecryptedEnv.mockResolvedValueOnce({}) mockRunStreamLoop.mockImplementationOnce( @@ -444,7 +433,7 @@ describe('runCopilotLifecycle', () => { }) it('runs legacy-v0 during Sim-first deployment without guessed billing aliases', async () => { - mockFlags.isHosted = true + setEnvFlags({ isHosted: true }) mockEnv.COPILOT_API_KEY = 'sim-agent-key' mockGetEffectiveDecryptedEnv.mockResolvedValueOnce({}) @@ -479,8 +468,8 @@ describe('runCopilotLifecycle', () => { }) it('runs modern hosted work without legacy compatibility storage', async () => { - mockFlags.isHosted = true - mockFlags.isCopilotBillingAttributionV1Enabled = true + setEnvFlags({ isHosted: true }) + setEnvFlags({ isCopilotBillingAttributionV1Enabled: true }) mockGetEffectiveDecryptedEnv.mockResolvedValueOnce({}) await runCopilotLifecycle( diff --git a/apps/sim/lib/copilot/request/lifecycle/start.test.ts b/apps/sim/lib/copilot/request/lifecycle/start.test.ts index c3da25457c2..30733864ff7 100644 --- a/apps/sim/lib/copilot/request/lifecycle/start.test.ts +++ b/apps/sim/lib/copilot/request/lifecycle/start.test.ts @@ -5,7 +5,7 @@ import { propagation, trace } from '@opentelemetry/api' import { W3CTraceContextPropagator } from '@opentelemetry/core' import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base' -import { dbChainMock, resetDbChainMock } from '@sim/testing' +import { dbChainMock, resetDbChainMock, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { MothershipStreamV1CompletionStatus, @@ -26,7 +26,6 @@ const { hasAbortMarker, releasePendingChatStream, fetchGo, - billingFlags, } = vi.hoisted(() => ({ runCopilotLifecycle: vi.fn(), createRunSegment: vi.fn(), @@ -41,10 +40,6 @@ const { hasAbortMarker: vi.fn(), releasePendingChatStream: vi.fn(), fetchGo: vi.fn(), - billingFlags: { - isHosted: false, - isCopilotBillingAttributionV1Enabled: false, - }, })) const BILLING_ATTRIBUTION = { @@ -133,15 +128,6 @@ vi.mock('@/lib/copilot/server/agent-url', () => ({ getMothershipSourceEnvHeaders: vi.fn().mockReturnValue({}), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return billingFlags.isHosted - }, - get isCopilotBillingAttributionV1Enabled() { - return billingFlags.isCopilotBillingAttributionV1Enabled - }, -})) - import { createSSEStream, requestChatTitle } from './start' async function drainStream(stream: ReadableStream) { @@ -152,6 +138,8 @@ async function drainStream(stream: ReadableStream) { } } +afterAll(resetEnvFlagsMock) + describe('createSSEStream terminal error handling', () => { afterAll(() => { resetDbChainMock() @@ -160,8 +148,8 @@ describe('createSSEStream terminal error handling', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - billingFlags.isHosted = false - billingFlags.isCopilotBillingAttributionV1Enabled = false + setEnvFlags({ isHosted: false }) + setEnvFlags({ isCopilotBillingAttributionV1Enabled: false }) fetchGo.mockResolvedValue( new Response(JSON.stringify({ title: 'Test title' }), { status: 200, @@ -347,8 +335,8 @@ describe('requestChatTitle billing protocol', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - billingFlags.isHosted = true - billingFlags.isCopilotBillingAttributionV1Enabled = true + setEnvFlags({ isHosted: true }) + setEnvFlags({ isCopilotBillingAttributionV1Enabled: true }) fetchGo.mockResolvedValue( new Response(JSON.stringify({ title: 'Billing Protocol' }), { status: 200, @@ -382,7 +370,7 @@ describe('requestChatTitle billing protocol', () => { }) it('sends explicit legacy-v0 during the Sim-first compatibility stage', async () => { - billingFlags.isCopilotBillingAttributionV1Enabled = false + setEnvFlags({ isCopilotBillingAttributionV1Enabled: false }) await requestChatTitle({ message: 'explain billing', diff --git a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts index f72695cb539..73a1b4d4892 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts @@ -1,10 +1,10 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { e2bFlag, betaFlag, mockLoadCompiledDoc, mockRunSandboxTask } = vi.hoisted(() => ({ - e2bFlag: { value: true }, +const { betaFlag, mockLoadCompiledDoc, mockRunSandboxTask } = vi.hoisted(() => ({ betaFlag: { value: false }, mockLoadCompiledDoc: vi.fn(), mockRunSandboxTask: vi.fn(), @@ -31,11 +31,6 @@ vi.mock('./doc-compiled-store', () => ({ vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: vi.fn(async () => betaFlag.value), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isE2BDocEnabled() { - return e2bFlag.value - }, -})) vi.mock('@/app/api/files/utils', () => ({ getContentType: (name: string) => name.endsWith('.pdf') @@ -53,10 +48,12 @@ const PDF_SOURCE = Buffer.from('from reportlab.pdfgen import canvas\n# generates const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x01]) const XLSX_SOURCE = Buffer.from('from openpyxl import Workbook\n# generates an xlsx', 'utf-8') +afterAll(resetEnvFlagsMock) + describe('resolveServableDocBytes', () => { beforeEach(() => { vi.clearAllMocks() - e2bFlag.value = true + setEnvFlags({ isE2BDocEnabled: true }) betaFlag.value = false }) @@ -93,7 +90,7 @@ describe('resolveServableDocBytes', () => { it('throws DocCompileUserError when a generated doc artifact is not ready (E2B regime)', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - e2bFlag.value = true + setEnvFlags({ isE2BDocEnabled: true }) await expect( resolveServableDocBytes({ @@ -108,7 +105,7 @@ describe('resolveServableDocBytes', () => { it('compiles via the sandbox when E2B is disabled and no artifact is stored', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - e2bFlag.value = false + setEnvFlags({ isE2BDocEnabled: false }) const compiled = Buffer.from('%PDF-isolated-vm-binary') mockRunSandboxTask.mockResolvedValue(compiled) @@ -153,7 +150,7 @@ describe('resolveServableDocBytes', () => { it('throws when a generated XLSX artifact is not ready (E2B + mothership-beta enabled)', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - e2bFlag.value = true + setEnvFlags({ isE2BDocEnabled: true }) betaFlag.value = true await expect( diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index e9d8c30e5d4..85ed283550b 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -1,21 +1,19 @@ /** * @vitest-environment node */ -import { envFlagsMock } from '@sim/testing' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { normalizeConditionRouterIds } from './builders' const { mockValidateSelectorIds, mockGetModelOptions, - mockEnvFlags, mockGetTool, mockGetCustomToolById, mockGetSkillById, } = vi.hoisted(() => ({ mockValidateSelectorIds: vi.fn(), mockGetModelOptions: vi.fn(() => []), - mockEnvFlags: { isHosted: false }, mockGetTool: vi.fn(), mockGetCustomToolById: vi.fn(), mockGetSkillById: vi.fn(), @@ -228,13 +226,6 @@ vi.mock('@/lib/workflows/skills/operations', () => ({ getSkillById: mockGetSkillById, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - ...envFlagsMock, - get isHosted() { - return mockEnvFlags.isHosted - }, -})) - vi.mock('@/providers/utils', () => ({ getHostedModels: () => [], })) @@ -247,6 +238,8 @@ import { validateWorkflowSelectorIds, } from './validation' +afterAll(resetEnvFlagsMock) + describe('validateInputsForBlock', () => { beforeEach(() => { vi.clearAllMocks() @@ -533,11 +526,11 @@ describe('preValidateCredentialInputs (hosted-tool blocks)', () => { vi.clearAllMocks() mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: [] }) mockGetTool.mockImplementation((id: string) => toolsByIdMock[id]) - mockEnvFlags.isHosted = true + setEnvFlags({ isHosted: true }) }) afterEach(() => { - mockEnvFlags.isHosted = false + setEnvFlags({ isHosted: false }) }) const ctx = { userId: 'user-1', workspaceId: 'workspace-1' } @@ -871,7 +864,7 @@ describe('preValidateCredentialInputs (hosted-tool blocks)', () => { }) it('preserves apiKey on self-hosted deployments (isHosted false)', async () => { - mockEnvFlags.isHosted = false + setEnvFlags({ isHosted: false }) const operations = [ { operation_type: 'add' as const, diff --git a/apps/sim/lib/core/config/block-visibility.test.ts b/apps/sim/lib/core/config/block-visibility.test.ts index ef7e44c1006..55fabe28702 100644 --- a/apps/sim/lib/core/config/block-visibility.test.ts +++ b/apps/sim/lib/core/config/block-visibility.test.ts @@ -1,16 +1,16 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { envFlagsMockFns, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockFetch, mockIsPlatformAdmin, envRef, flagRef } = vi.hoisted(() => ({ +const { mockFetch, mockIsPlatformAdmin, envRef } = vi.hoisted(() => ({ mockFetch: vi.fn(), mockIsPlatformAdmin: vi.fn(), envRef: { APPCONFIG_APPLICATION: 'sim-staging' as string | undefined, APPCONFIG_ENVIRONMENT: 'staging' as string | undefined, }, - flagRef: { isAppConfigEnabled: false, previewBlocks: [] as string[] }, })) vi.mock('@/lib/core/config/appconfig', () => ({ @@ -23,13 +23,6 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isAppConfigEnabled() { - return flagRef.isAppConfigEnabled - }, - getPreviewBlocksFromEnv: () => flagRef.previewBlocks, -})) - vi.mock('@/lib/permissions/super-user', () => ({ isPlatformAdmin: mockIsPlatformAdmin, })) @@ -38,20 +31,22 @@ import { getBlockVisibility } from '@/lib/core/config/block-visibility' /** Make `getBlockVisibility` resolve `doc` via the AppConfig path (also exercises parsing). */ function withAppConfig(doc: unknown) { - flagRef.isAppConfigEnabled = true + setEnvFlags({ isAppConfigEnabled: true }) mockFetch.mockImplementation((_ids, parse) => Promise.resolve(parse(doc))) } +afterAll(resetEnvFlagsMock) + describe('getBlockVisibility', () => { beforeEach(() => { vi.clearAllMocks() - flagRef.isAppConfigEnabled = false - flagRef.previewBlocks = [] + setEnvFlags({ isAppConfigEnabled: false }) + envFlagsMockFns.getPreviewBlocksFromEnv.mockReturnValue([]) }) describe('off-AppConfig (env fallback)', () => { it('reveals and preview-tags the PREVIEW_BLOCKS types without fetching', async () => { - flagRef.previewBlocks = ['gmail_v2', 'notion_v3'] + envFlagsMockFns.getPreviewBlocksFromEnv.mockReturnValue(['gmail_v2', 'notion_v3']) const vis = await getBlockVisibility({ userId: 'u1' }) expect(vis.revealed).toEqual(new Set(['gmail_v2', 'notion_v3'])) expect(vis.previewTagged).toEqual(new Set(['gmail_v2', 'notion_v3'])) diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts index de017d9826c..aae8a7a2b6b 100644 --- a/apps/sim/lib/core/config/feature-flags.test.ts +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -1,10 +1,11 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { FeatureFlagContext, FeatureFlagName } from '@/lib/core/config/feature-flags' -const { mockFetch, mockIsPlatformAdmin, envRef, flagRef } = vi.hoisted(() => ({ +const { mockFetch, mockIsPlatformAdmin, envRef } = vi.hoisted(() => ({ mockFetch: vi.fn(), mockIsPlatformAdmin: vi.fn(), envRef: { @@ -13,7 +14,6 @@ const { mockFetch, mockIsPlatformAdmin, envRef, flagRef } = vi.hoisted(() => ({ FORKING_ENABLED: undefined as boolean | undefined, DEPLOY_AS_BLOCK: undefined as boolean | undefined, }, - flagRef: { isAppConfigEnabled: false }, })) vi.mock('@/lib/core/config/appconfig', () => ({ @@ -27,12 +27,6 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isAppConfigEnabled() { - return flagRef.isAppConfigEnabled - }, -})) - vi.mock('@/lib/permissions/super-user', () => ({ isPlatformAdmin: mockIsPlatformAdmin, })) @@ -58,7 +52,7 @@ import { /** Make `getFeatureFlags` resolve to `doc` via the AppConfig path (also exercises parseConfig). */ function withAppConfig(doc: unknown) { - flagRef.isAppConfigEnabled = true + setEnvFlags({ isAppConfigEnabled: true }) mockFetch.mockImplementation((_ids, parse) => Promise.resolve(parse(doc))) } @@ -70,10 +64,12 @@ function withAppConfig(doc: unknown) { const enabled = (flag: string, ctx?: FeatureFlagContext) => isFeatureEnabled(flag as FeatureFlagName, ctx) +afterAll(resetEnvFlagsMock) + describe('getFeatureFlags', () => { beforeEach(() => { vi.clearAllMocks() - flagRef.isAppConfigEnabled = false + setEnvFlags({ isAppConfigEnabled: false }) }) it('derives flags from fallback secrets when AppConfig is disabled, without fetching', async () => { @@ -104,7 +100,7 @@ describe('getFeatureFlags', () => { }) it('falls back to the secret-derived document when the fetch yields null', async () => { - flagRef.isAppConfigEnabled = true + setEnvFlags({ isAppConfigEnabled: true }) mockFetch.mockResolvedValue(null) const flags = await getFeatureFlags() expect(flags['mothership-beta']).toEqual({ enabled: false }) @@ -124,7 +120,7 @@ describe('getFeatureFlags', () => { describe('isFeatureEnabled', () => { beforeEach(() => { vi.clearAllMocks() - flagRef.isAppConfigEnabled = false + setEnvFlags({ isAppConfigEnabled: false }) envRef.FORKING_ENABLED = undefined envRef.DEPLOY_AS_BLOCK = undefined }) diff --git a/apps/sim/lib/core/execution-limits/types.test.ts b/apps/sim/lib/core/execution-limits/types.test.ts index a166a9d7496..a6a78777419 100644 --- a/apps/sim/lib/core/execution-limits/types.test.ts +++ b/apps/sim/lib/core/execution-limits/types.test.ts @@ -1,28 +1,22 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' /** * Free-tier timeouts are baked into EXECUTION_TIMEOUTS at module load, while * the billing-disabled opt-in check reads the env at call time. Seeding here * mirrors production, where both reads observe the same process env. */ -const { mockEnv, mockFlags } = vi.hoisted(() => ({ +const { mockEnv } = vi.hoisted(() => ({ mockEnv: { EXECUTION_TIMEOUT_FREE: '120', EXECUTION_TIMEOUT_ASYNC_FREE: '240', } as Record, - mockFlags: { isBillingEnabled: true }, })) vi.mock('@/lib/core/config/env', () => ({ env: mockEnv })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) - /** * Query-suffixed import gives this file a private instance of the module under * test (the barrel's `./types` source, so the fresh evaluation bakes the mocked @@ -43,9 +37,11 @@ import { getExecutionTimeout, } from '@/lib/core/execution-limits/types?execution-limits-test' +afterAll(resetEnvFlagsMock) + describe('getExecutionTimeout', () => { beforeEach(() => { - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockEnv.EXECUTION_TIMEOUT_FREE = '120' mockEnv.EXECUTION_TIMEOUT_ASYNC_FREE = '240' }) @@ -57,7 +53,7 @@ describe('getExecutionTimeout', () => { }) it('disables timeouts when billing is disabled and no free env is set', () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockEnv.EXECUTION_TIMEOUT_FREE = undefined mockEnv.EXECUTION_TIMEOUT_ASYNC_FREE = undefined @@ -66,7 +62,7 @@ describe('getExecutionTimeout', () => { }) it('opts back into the free timeout when the env var is explicitly set', () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) expect(getExecutionTimeout('free', 'sync')).toBe(120 * 1000) expect(getExecutionTimeout('free', 'async')).toBe(240 * 1000) diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts index 48d14f671d7..67a3f332719 100644 --- a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts +++ b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, type Mock, vi } from 'vitest' /** * Query-suffixed imports give this file private instances of the modules under @@ -20,7 +21,6 @@ declare module '@/lib/core/rate-limiter/types?rate-limiter-test' { export * from '@/lib/core/rate-limiter/types' } -vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true })) vi.mock( '@/lib/core/rate-limiter/types', () => import('@/lib/core/rate-limiter/types?rate-limiter-test') @@ -42,6 +42,12 @@ const createMockAdapter = (): MockAdapter => ({ resetBucket: vi.fn(), }) +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('RateLimiter', () => { const testUserId = 'test-user-123' const freeSubscription = { plan: 'free', referenceId: testUserId } diff --git a/apps/sim/lib/core/rate-limiter/types.test.ts b/apps/sim/lib/core/rate-limiter/types.test.ts index 1a33c9c642a..26b59b224df 100644 --- a/apps/sim/lib/core/rate-limiter/types.test.ts +++ b/apps/sim/lib/core/rate-limiter/types.test.ts @@ -1,33 +1,30 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' /** * Free-tier env values are baked into RATE_LIMITS at module load, while the * billing-disabled opt-in check reads them at call time. Seeding them here * mirrors production, where both reads observe the same process env. */ -const { mockEnv, mockFlags } = vi.hoisted(() => ({ +const { mockEnv } = vi.hoisted(() => ({ mockEnv: { RATE_LIMIT_FREE_SYNC: '25', RATE_LIMIT_FREE_API_ENDPOINT: '10', } as Record, - mockFlags: { isBillingEnabled: true }, })) vi.mock('@/lib/core/config/env', () => ({ env: mockEnv })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) import { getRateLimit } from '@/lib/core/rate-limiter/types' +afterAll(resetEnvFlagsMock) + describe('getRateLimit', () => { beforeEach(() => { - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockEnv.RATE_LIMIT_FREE_SYNC = '25' mockEnv.RATE_LIMIT_FREE_API_ENDPOINT = '10' }) @@ -40,7 +37,7 @@ describe('getRateLimit', () => { }) it('is effectively unlimited when billing is disabled and no free env is set', () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockEnv.RATE_LIMIT_FREE_SYNC = undefined mockEnv.RATE_LIMIT_FREE_API_ENDPOINT = undefined @@ -50,7 +47,7 @@ describe('getRateLimit', () => { }) it('opts back into enforcement per counter when a free env var is explicitly set', () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) expect(getRateLimit('free', 'sync').refillRate).toBe(25) expect(getRateLimit('free', 'api-endpoint').refillRate).toBe(10) diff --git a/apps/sim/lib/core/security/csp.test.ts b/apps/sim/lib/core/security/csp.test.ts index a91c97d6501..1cf199dc08c 100644 --- a/apps/sim/lib/core/security/csp.test.ts +++ b/apps/sim/lib/core/security/csp.test.ts @@ -1,4 +1,4 @@ -import { createEnvMock, envFlagsMock } from '@sim/testing' +import { createEnvMock } from '@sim/testing' import { afterEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/config/env', () => @@ -17,8 +17,6 @@ vi.mock('@/lib/core/config/env', () => }) ) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - import { addCSPSource, buildCSPString, diff --git a/apps/sim/lib/core/security/input-validation.test.ts b/apps/sim/lib/core/security/input-validation.test.ts index eb44eb55939..e6d90a1518e 100644 --- a/apps/sim/lib/core/security/input-validation.test.ts +++ b/apps/sim/lib/core/security/input-validation.test.ts @@ -1,5 +1,5 @@ -import { envFlagsMock } from '@sim/testing' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { envFlagsMock, resetEnvFlagsMock } from '@sim/testing' +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' import { validateAirtableId, validateAlphanumericId, @@ -34,7 +34,7 @@ import { } from '@/lib/core/security/input-validation.server' import { sanitizeForLogging } from '@/lib/core/security/redaction' -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) +afterAll(resetEnvFlagsMock) describe('validatePathSegment', () => { describe('valid inputs', () => { diff --git a/apps/sim/lib/core/security/pinned-fetch.server.test.ts b/apps/sim/lib/core/security/pinned-fetch.server.test.ts index 292439034fe..66c798ad556 100644 --- a/apps/sim/lib/core/security/pinned-fetch.server.test.ts +++ b/apps/sim/lib/core/security/pinned-fetch.server.test.ts @@ -1,7 +1,6 @@ /** * @vitest-environment node */ -import { envFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockAgent, mockUndiciFetch, capturedAgentOptions, agentCloses } = vi.hoisted(() => { @@ -25,8 +24,6 @@ const { mockAgent, mockUndiciFetch, capturedAgentOptions, agentCloses } = vi.hoi }) vi.mock('undici', () => ({ Agent: mockAgent, fetch: mockUndiciFetch })) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - /** * Query-suffixed import gives this file a private instance of the module under * test. Under `isolate: false` the worker's module graph is shared across test diff --git a/apps/sim/lib/core/utils/urls.test.ts b/apps/sim/lib/core/utils/urls.test.ts index f32618d3ee5..55cc21e71a5 100644 --- a/apps/sim/lib/core/utils/urls.test.ts +++ b/apps/sim/lib/core/utils/urls.test.ts @@ -12,10 +12,6 @@ vi.mock('@/lib/core/config/env', () => ({ getEnv: mockGetEnv, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isProd: false, -})) - import { getBrowserOrigin, getSocketUrl, diff --git a/apps/sim/lib/data-drains/dispatcher.test.ts b/apps/sim/lib/data-drains/dispatcher.test.ts index 0d94887b7dd..17edc85de08 100644 --- a/apps/sim/lib/data-drains/dispatcher.test.ts +++ b/apps/sim/lib/data-drains/dispatcher.test.ts @@ -1,8 +1,14 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/db', () => dbChainMock) @@ -19,7 +25,6 @@ vi.mock('@/lib/billing/core/subscription', () => ({ isOrganizationOnEnterprisePlan: mockIsEnterprise, })) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: mockGetJobQueue })) -vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true })) import { dispatchDueDrains, reapOrphanedRuns } from '@/lib/data-drains/dispatcher' @@ -36,6 +41,12 @@ beforeEach(() => { resetDbChainMock() }) +beforeAll(() => { + setEnvFlags({ isBillingEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('reapOrphanedRuns', () => { it('returns the count of rows updated to failed', async () => { dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'run-1' }, { id: 'run-2' }]) diff --git a/apps/sim/lib/invitations/core.test.ts b/apps/sim/lib/invitations/core.test.ts index 0050080ea9c..a3f258e0a02 100644 --- a/apps/sim/lib/invitations/core.test.ts +++ b/apps/sim/lib/invitations/core.test.ts @@ -1,8 +1,15 @@ /** * @vitest-environment node */ -import { auditMock, dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + auditMock, + dbChainMock, + dbChainMockFns, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockEnsureUserInOrganization, @@ -16,7 +23,6 @@ const { mockSyncUsageLimitsFromSubscription, mockSyncWorkspaceEnvCredentials, mockIsWorkspaceOnEnterprisePlan, - mockFeatureFlags, } = vi.hoisted(() => ({ mockEnsureUserInOrganization: vi.fn(), mockGetUserOrganization: vi.fn(), @@ -29,7 +35,6 @@ const { mockSyncUsageLimitsFromSubscription: vi.fn(), mockSyncWorkspaceEnvCredentials: vi.fn(), mockIsWorkspaceOnEnterprisePlan: vi.fn(async () => true), - mockFeatureFlags: { isBillingEnabled: true }, })) vi.mock('@sim/db', () => dbChainMock) @@ -53,12 +58,6 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceWithOwner: mockGetWorkspaceWithOwner, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFeatureFlags.isBillingEnabled - }, -})) - vi.mock('@/lib/auth/active-organization', () => ({ setActiveOrganizationForCurrentSession: mockSetActiveOrganizationForCurrentSession, })) @@ -104,11 +103,13 @@ function executedSqlContaining(substring: string): boolean { }) } +afterAll(resetEnvFlagsMock) + describe('acceptInvitation', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetUserOrganization.mockResolvedValue(null) mockGetWorkspaceWithOwner.mockResolvedValue(null) mockEnsureTeamOrganizationForAcceptance.mockResolvedValue({ diff --git a/apps/sim/lib/knowledge/documents/processing-queue.test.ts b/apps/sim/lib/knowledge/documents/processing-queue.test.ts index 26944e5d52c..62de6d9d01d 100644 --- a/apps/sim/lib/knowledge/documents/processing-queue.test.ts +++ b/apps/sim/lib/knowledge/documents/processing-queue.test.ts @@ -1,8 +1,15 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns, defaultMockEnv, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { + dbChainMock, + dbChainMockFns, + defaultMockEnv, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution' import { env } from '@/lib/core/config/env' @@ -33,10 +40,6 @@ afterAll(() => { } Object.assign(env, envSnapshot) }) -vi.mock('@/lib/core/config/env-flags', () => ({ - getCostMultiplier: vi.fn().mockReturnValue(1), - isTriggerDevEnabled: true, -})) import { processDocumentsWithQueue } from '@/lib/knowledge/documents/service' @@ -61,6 +64,12 @@ const DOCUMENT = { mimeType: 'text/plain', } +beforeAll(() => { + setEnvFlags({ isTriggerDevEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('processDocumentsWithQueue billing attribution', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/logs/execution/logger.test.ts b/apps/sim/lib/logs/execution/logger.test.ts index 740986611fb..5ae0036ed00 100644 --- a/apps/sim/lib/logs/execution/logger.test.ts +++ b/apps/sim/lib/logs/execution/logger.test.ts @@ -1,5 +1,5 @@ import { usageLog, workflow } from '@sim/db/schema' -import { dbChainMockFns, envFlagsMock, queueTableRows, resetDbChainMock } from '@sim/testing' +import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest' import { recordUsage } from '@/lib/billing/core/usage-log' import { ExecutionLogger } from '@/lib/logs/execution/logger' @@ -70,8 +70,6 @@ vi.mock('@/lib/billing/threshold-billing', () => ({ checkAndBillPayerOverageThreshold: vi.fn(() => Promise.resolve()), })) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - // Mock security module vi.mock('@/lib/core/security/redaction', () => ({ redactApiKeys: vi.fn((data) => data), diff --git a/apps/sim/lib/mcp/connection-manager.test.ts b/apps/sim/lib/mcp/connection-manager.test.ts index f4845b099ec..47c91f79b28 100644 --- a/apps/sim/lib/mcp/connection-manager.test.ts +++ b/apps/sim/lib/mcp/connection-manager.test.ts @@ -1,7 +1,8 @@ /** * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' interface MockMcpClient { connect: ReturnType @@ -40,7 +41,6 @@ const { mockGetOrCreateOauthRow: vi.fn(), })) -vi.mock('@/lib/core/config/env-flags', () => ({ isTest: false })) vi.mock('@/lib/mcp/pubsub', () => ({ mcpPubSub: { onToolsChanged: mockOnToolsChanged, @@ -64,6 +64,12 @@ vi.mock('@/lib/mcp/oauth', () => ({ import { McpConnectionManager } from '@/lib/mcp/connection-manager' +beforeAll(() => { + setEnvFlags({ isTest: false }) +}) + +afterAll(resetEnvFlagsMock) + describe('McpConnectionManager', () => { let manager: McpConnectionManager | null = null diff --git a/apps/sim/lib/mcp/domain-check.test.ts b/apps/sim/lib/mcp/domain-check.test.ts index ab0616c6ac2..b30b2d373fd 100644 --- a/apps/sim/lib/mcp/domain-check.test.ts +++ b/apps/sim/lib/mcp/domain-check.test.ts @@ -1,20 +1,17 @@ /** * @vitest-environment node */ -import { inputValidationMock, inputValidationMockFns } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockGetAllowedMcpDomainsFromEnv, mockDnsLookup, hostedFlag } = vi.hoisted(() => ({ - mockGetAllowedMcpDomainsFromEnv: vi.fn<() => string[] | null>(), +import { + envFlagsMockFns, + inputValidationMock, + inputValidationMockFns, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDnsLookup } = vi.hoisted(() => ({ mockDnsLookup: vi.fn(), - hostedFlag: { value: false }, -})) - -vi.mock('@/lib/core/config/env-flags', () => ({ - getAllowedMcpDomainsFromEnv: mockGetAllowedMcpDomainsFromEnv, - get isHosted() { - return hostedFlag.value - }, })) vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) @@ -48,6 +45,10 @@ import { validateMcpServerSsrf, } from './domain-check' +const mockGetAllowedMcpDomainsFromEnv = envFlagsMockFns.getAllowedMcpDomainsFromEnv + +afterAll(resetEnvFlagsMock) + describe('McpDomainNotAllowedError', () => { it.concurrent('creates error with correct name and message', () => { const error = new McpDomainNotAllowedError('evil.com') @@ -335,7 +336,7 @@ describe('validateMcpServerSsrf', () => { beforeEach(() => { vi.clearAllMocks() mockGetAllowedMcpDomainsFromEnv.mockReturnValue(null) - hostedFlag.value = false + setEnvFlags({ isHosted: false }) }) it('returns null for undefined URL', async () => { @@ -453,7 +454,7 @@ describe('validateMcpServerSsrf', () => { describe('hosted environment', () => { beforeEach(() => { - hostedFlag.value = true + setEnvFlags({ isHosted: true }) }) it('rejects localhost URLs on hosted', async () => { @@ -515,7 +516,7 @@ describe('validateMcpServerSsrf', () => { describe('self-hosted environment (regression)', () => { beforeEach(() => { - hostedFlag.value = false + setEnvFlags({ isHosted: false }) }) it('still allows localhost URLs (returns null, no pinning needed)', async () => { diff --git a/apps/sim/lib/mcp/service-pool.test.ts b/apps/sim/lib/mcp/service-pool.test.ts index 406471bcced..ac334e22b41 100644 --- a/apps/sim/lib/mcp/service-pool.test.ts +++ b/apps/sim/lib/mcp/service-pool.test.ts @@ -61,7 +61,6 @@ const { }) vi.mock('@sim/logger', () => loggerMock) -vi.mock('@/lib/core/config/env-flags', () => ({ isTest: true })) vi.mock('@/lib/mcp/connection-pool', () => ({ mcpConnectionPool: { acquire: mockAcquire, evictServer: vi.fn() }, })) diff --git a/apps/sim/lib/table/billing.test.ts b/apps/sim/lib/table/billing.test.ts index ec71d49c9bf..edcc9dcf882 100644 --- a/apps/sim/lib/table/billing.test.ts +++ b/apps/sim/lib/table/billing.test.ts @@ -1,16 +1,15 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { - mockFlags, mockResolveWorkspaceBillingPayer, mockGetPlanTypeForLimits, mockGetBillingDisabledTableLimits, mockGetTablePlanLimits, } = vi.hoisted(() => ({ - mockFlags: { isBillingEnabled: true }, mockResolveWorkspaceBillingPayer: vi.fn(), mockGetPlanTypeForLimits: vi.fn(), mockGetBillingDisabledTableLimits: vi.fn(), @@ -23,11 +22,6 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({ vi.mock('@/lib/billing/plan-helpers', () => ({ getPlanTypeForLimits: mockGetPlanTypeForLimits, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) vi.mock('@/lib/table/constants', () => ({ getBillingDisabledTableLimits: mockGetBillingDisabledTableLimits, getTablePlanLimits: mockGetTablePlanLimits, @@ -56,7 +50,7 @@ const nextWorkspaceId = () => `ws-${++wsCounter}` beforeEach(() => { vi.clearAllMocks() - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetTablePlanLimits.mockReturnValue(LIMITS) mockGetBillingDisabledTableLimits.mockReturnValue({ maxTables: Number.MAX_SAFE_INTEGER, @@ -70,6 +64,8 @@ beforeEach(() => { mockGetPlanTypeForLimits.mockReturnValue('pro') }) +afterAll(resetEnvFlagsMock) + describe('getWorkspaceTableLimits', () => { it('returns the limits for the workspace subscription plan', async () => { expect(await getWorkspaceTableLimits(nextWorkspaceId())).toEqual(LIMITS.pro) @@ -96,7 +92,7 @@ describe('getWorkspaceTableLimits', () => { }) it('bypasses billing plan resolution entirely when billing is disabled', async () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetBillingDisabledTableLimits.mockReturnValue({ maxTables: Number.MAX_SAFE_INTEGER, maxRowsPerTable: 12345, diff --git a/apps/sim/lib/table/dispatch-concurrency.test.ts b/apps/sim/lib/table/dispatch-concurrency.test.ts index 2581b6e987b..770be25bb1b 100644 --- a/apps/sim/lib/table/dispatch-concurrency.test.ts +++ b/apps/sim/lib/table/dispatch-concurrency.test.ts @@ -1,11 +1,11 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockEnv, mockFlags } = vi.hoisted(() => ({ +const { mockEnv } = vi.hoisted(() => ({ mockEnv: {} as Record, - mockFlags: { isBillingEnabled: true }, })) vi.mock('@/lib/core/config/env', () => ({ @@ -25,21 +25,17 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFlags.isBillingEnabled - }, -})) - import { getMaxTableDispatchConcurrency, getTableDispatchConcurrency, } from '@/lib/table/dispatch-concurrency' +afterAll(resetEnvFlagsMock) + describe('getTableDispatchConcurrency', () => { beforeEach(() => { for (const key of Object.keys(mockEnv)) delete mockEnv[key] - mockFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) }) it('resolves free vs paid defaults', () => { @@ -60,7 +56,7 @@ describe('getTableDispatchConcurrency', () => { }) it('uses the paid value when billing is disabled', () => { - mockFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) expect(getTableDispatchConcurrency(null)).toBe(50) mockEnv.TABLE_DISPATCH_CONCURRENCY_PAID = '120' diff --git a/apps/sim/lib/table/workflow-columns.test.ts b/apps/sim/lib/table/workflow-columns.test.ts index 544344a114e..c516d06c3e1 100644 --- a/apps/sim/lib/table/workflow-columns.test.ts +++ b/apps/sim/lib/table/workflow-columns.test.ts @@ -1,7 +1,8 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { RowExecutionMetadata, TableDefinition, @@ -33,10 +34,6 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({ resolveSystemBillingAttribution: mockResolveSystemBillingAttribution, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - isTriggerDevEnabled: true, -})) - import { buildEnqueueItems, pickNextEligibleGroupForRow, @@ -104,6 +101,12 @@ function queuedMarker(workflowId: string): RowExecutionMetadata { return { status: 'pending', executionId: null, jobId: null, workflowId, error: null } } +beforeAll(() => { + setEnvFlags({ isTriggerDevEnabled: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('pickNextEligibleGroupForRow — queued-marker handoff', () => { it('runs an autoRun:false group that carries a queued marker (explicit request)', () => { const group = makeGroup({ id: 'g1', autoRun: false }) diff --git a/apps/sim/lib/webhooks/processor.test.ts b/apps/sim/lib/webhooks/processor.test.ts index 21d5c00eee0..9218ab35403 100644 --- a/apps/sim/lib/webhooks/processor.test.ts +++ b/apps/sim/lib/webhooks/processor.test.ts @@ -6,7 +6,6 @@ import type { webhook, workflow } from '@sim/db/schema' import { createMockRequest, dbChainMock, - envFlagsMock, executionPreprocessingMock, executionPreprocessingMockFns, queueTableRows, @@ -78,8 +77,6 @@ vi.mock('@/lib/core/admission/gate', () => ({ tryAdmit: vi.fn(() => ({ release: mockAdmissionRelease })), })) -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - vi.mock('@sim/security/compare', () => ({ safeCompare: vi.fn().mockReturnValue(true), })) diff --git a/apps/sim/lib/workspaces/policy.test.ts b/apps/sim/lib/workspaces/policy.test.ts index 0fc262b6036..fc5f2f86b0e 100644 --- a/apps/sim/lib/workspaces/policy.test.ts +++ b/apps/sim/lib/workspaces/policy.test.ts @@ -2,19 +2,23 @@ * @vitest-environment node */ import { member, workspace } from '@sim/db/schema' -import { dbChainMock, queueTableRows, resetDbChainMock } from '@sim/testing' +import { + dbChainMock, + queueTableRows, + resetDbChainMock, + resetEnvFlagsMock, + setEnvFlags, +} from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetUserOrganization, mockGetOrganizationSubscription, mockGetHighestPrioritySubscription, - mockFeatureFlags, } = vi.hoisted(() => ({ mockGetUserOrganization: vi.fn(), mockGetOrganizationSubscription: vi.fn(), mockGetHighestPrioritySubscription: vi.fn(), - mockFeatureFlags: { isBillingEnabled: true }, })) vi.mock('@sim/db', () => dbChainMock) @@ -31,12 +35,6 @@ vi.mock('@/lib/billing/core/plan', () => ({ getHighestPrioritySubscription: mockGetHighestPrioritySubscription, })) -vi.mock('@/lib/core/config/env-flags', () => ({ - get isBillingEnabled() { - return mockFeatureFlags.isBillingEnabled - }, -})) - import { getWorkspaceCreationPolicy, getWorkspaceInvitePolicy, @@ -46,11 +44,13 @@ import { UPGRADE_TO_INVITE_REASON } from '@/lib/workspaces/policy-constants' afterAll(resetDbChainMock) +afterAll(resetEnvFlagsMock) + describe('getWorkspaceCreationPolicy', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetUserOrganization.mockResolvedValue(null) mockGetOrganizationSubscription.mockResolvedValue(null) mockGetHighestPrioritySubscription.mockResolvedValue(null) @@ -115,7 +115,7 @@ describe('getWorkspaceCreationPolicy', () => { }) it('allows unlimited personal workspaces when billing is disabled', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) queueTableRows(workspace, [{ value: 9 }]) const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) @@ -128,7 +128,7 @@ describe('getWorkspaceCreationPolicy', () => { }) it('without pinning, a null active org falls back to the caller membership org', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetUserOrganization.mockResolvedValue({ organizationId: 'user-org', role: 'admin', @@ -146,7 +146,7 @@ describe('getWorkspaceCreationPolicy', () => { }) it('pins to the source org: a personal source (null) stays personal regardless of caller org', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetUserOrganization.mockResolvedValue({ organizationId: 'user-org', role: 'admin', @@ -191,7 +191,7 @@ describe('getWorkspaceCreationPolicy', () => { }) it('allows org admins to create organization workspaces when billing is disabled', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'admin', @@ -257,7 +257,7 @@ describe('getWorkspaceInvitePolicy', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() - mockFeatureFlags.isBillingEnabled = true + setEnvFlags({ isBillingEnabled: true }) mockGetOrganizationSubscription.mockResolvedValue(null) mockGetHighestPrioritySubscription.mockResolvedValue(null) }) @@ -270,7 +270,7 @@ describe('getWorkspaceInvitePolicy', () => { } as const it('allows invites unconditionally when billing is disabled', async () => { - mockFeatureFlags.isBillingEnabled = false + setEnvFlags({ isBillingEnabled: false }) const result = await getWorkspaceInvitePolicy(baseState) diff --git a/apps/sim/providers/index.test.ts b/apps/sim/providers/index.test.ts index 73b62a79abc..2f52c842461 100644 --- a/apps/sim/providers/index.test.ts +++ b/apps/sim/providers/index.test.ts @@ -18,10 +18,6 @@ vi.mock('@/providers/registry', () => ({ }), })) -vi.mock('@/lib/core/config/env-flags', () => ({ - getCostMultiplier: vi.fn(() => 1), -})) - import { executeProviderRequest } from '@/providers' import type { ProviderResponse } from '@/providers/types' diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 87d576b79c5..cf5778d9db3 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -1,5 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import * as environmentModule from '@/lib/core/config/env-flags' +import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { calculateCost, extractAndParseJSON, @@ -43,19 +43,18 @@ import { updateOllamaProviderModels, } from '@/providers/utils' -const isHostedSpy = vi.spyOn(environmentModule, 'isHosted', 'get') as unknown as { - mockReturnValue: (value: boolean) => void -} const mockGetRotatingApiKey = vi.fn().mockReturnValue('rotating-server-key') const originalRequire = module.require +afterAll(resetEnvFlagsMock) + describe('getApiKey', () => { const originalEnv = { ...process.env } beforeEach(() => { vi.clearAllMocks() - isHostedSpy.mockReturnValue(false) + setEnvFlags({ isHosted: false }) module.require = vi.fn(() => ({ getRotatingApiKey: mockGetRotatingApiKey, @@ -67,8 +66,8 @@ describe('getApiKey', () => { module.require = originalRequire }) - it.concurrent('should return user-provided key when not in hosted environment', () => { - isHostedSpy.mockReturnValue(false) + it('should return user-provided key when not in hosted environment', () => { + setEnvFlags({ isHosted: false }) const key1 = getApiKey('openai', 'gpt-4', 'user-key-openai') expect(key1).toBe('user-key-openai') @@ -80,8 +79,8 @@ describe('getApiKey', () => { expect(key3).toBe('user-key-google') }) - it.concurrent('should throw error if no key provided in non-hosted environment', () => { - isHostedSpy.mockReturnValue(false) + it('should throw error if no key provided in non-hosted environment', () => { + setEnvFlags({ isHosted: false }) expect(() => getApiKey('openai', 'gpt-4')).toThrow('API key is required for openai gpt-4') expect(() => getApiKey('anthropic', 'claude-3')).toThrow( @@ -89,8 +88,8 @@ describe('getApiKey', () => { ) }) - it.concurrent('should fall back to user key in hosted environment if rotation fails', () => { - isHostedSpy.mockReturnValue(true) + it('should fall back to user key in hosted environment if rotation fails', () => { + setEnvFlags({ isHosted: true }) module.require = vi.fn(() => { throw new Error('Rotation failed') @@ -100,56 +99,47 @@ describe('getApiKey', () => { expect(key).toBe('user-fallback-key') }) - it.concurrent( - 'should throw error in hosted environment if rotation fails and no user key', - () => { - isHostedSpy.mockReturnValue(true) + it('should throw error in hosted environment if rotation fails and no user key', () => { + setEnvFlags({ isHosted: true }) - module.require = vi.fn(() => { - throw new Error('Rotation failed') - }) + module.require = vi.fn(() => { + throw new Error('Rotation failed') + }) - expect(() => getApiKey('openai', 'gpt-4o')).toThrow('No API key available for openai gpt-4o') - } - ) + expect(() => getApiKey('openai', 'gpt-4o')).toThrow('No API key available for openai gpt-4o') + }) - it.concurrent( - 'should require user key for non-OpenAI/Anthropic providers even in hosted environment', - () => { - isHostedSpy.mockReturnValue(true) + it('should require user key for non-OpenAI/Anthropic providers even in hosted environment', () => { + setEnvFlags({ isHosted: true }) - const key = getApiKey('other-provider', 'some-model', 'user-key') - expect(key).toBe('user-key') + const key = getApiKey('other-provider', 'some-model', 'user-key') + expect(key).toBe('user-key') - expect(() => getApiKey('other-provider', 'some-model')).toThrow( - 'API key is required for other-provider some-model' - ) - } - ) + expect(() => getApiKey('other-provider', 'some-model')).toThrow( + 'API key is required for other-provider some-model' + ) + }) - it.concurrent( - 'should require user key for models NOT in hosted list even if provider matches', - () => { - isHostedSpy.mockReturnValue(true) + it('should require user key for models NOT in hosted list even if provider matches', () => { + setEnvFlags({ isHosted: true }) - const key1 = getApiKey('anthropic', 'claude-sonnet-4-20250514', 'user-key-anthropic') - expect(key1).toBe('user-key-anthropic') + const key1 = getApiKey('anthropic', 'claude-sonnet-4-20250514', 'user-key-anthropic') + expect(key1).toBe('user-key-anthropic') - expect(() => getApiKey('anthropic', 'claude-sonnet-4-20250514')).toThrow( - 'API key is required for anthropic claude-sonnet-4-20250514' - ) + expect(() => getApiKey('anthropic', 'claude-sonnet-4-20250514')).toThrow( + 'API key is required for anthropic claude-sonnet-4-20250514' + ) - const key2 = getApiKey('openai', 'gpt-4o-2024-08-06', 'user-key-openai') - expect(key2).toBe('user-key-openai') + const key2 = getApiKey('openai', 'gpt-4o-2024-08-06', 'user-key-openai') + expect(key2).toBe('user-key-openai') - expect(() => getApiKey('openai', 'gpt-4o-2024-08-06')).toThrow( - 'API key is required for openai gpt-4o-2024-08-06' - ) - } - ) + expect(() => getApiKey('openai', 'gpt-4o-2024-08-06')).toThrow( + 'API key is required for openai gpt-4o-2024-08-06' + ) + }) - it.concurrent('should return empty for ollama provider without requiring API key', () => { - isHostedSpy.mockReturnValue(false) + it('should return empty for ollama provider without requiring API key', () => { + setEnvFlags({ isHosted: false }) const key = getApiKey('ollama', 'llama2') expect(key).toBe('empty') @@ -158,36 +148,30 @@ describe('getApiKey', () => { expect(key2).toBe('empty') }) - it.concurrent( - 'should return empty or user-provided key for vllm provider without requiring API key', - () => { - isHostedSpy.mockReturnValue(false) + it('should return empty or user-provided key for vllm provider without requiring API key', () => { + setEnvFlags({ isHosted: false }) - const key = getApiKey('vllm', 'vllm/qwen-3') - expect(key).toBe('empty') + const key = getApiKey('vllm', 'vllm/qwen-3') + expect(key).toBe('empty') - const key2 = getApiKey('vllm', 'vllm/llama', 'user-key') - expect(key2).toBe('user-key') - } - ) + const key2 = getApiKey('vllm', 'vllm/llama', 'user-key') + expect(key2).toBe('user-key') + }) - it.concurrent( - 'should return empty or user-provided key for litellm provider without requiring API key', - () => { - isHostedSpy.mockReturnValue(false) + it('should return empty or user-provided key for litellm provider without requiring API key', () => { + setEnvFlags({ isHosted: false }) - const key = getApiKey('litellm', 'litellm/anthropic/claude-sonnet-4-6') - expect(key).toBe('empty') + const key = getApiKey('litellm', 'litellm/anthropic/claude-sonnet-4-6') + expect(key).toBe('empty') - const key2 = getApiKey('litellm', 'litellm/openai/gpt-4', 'user-key') - expect(key2).toBe('user-key') - } - ) + const key2 = getApiKey('litellm', 'litellm/openai/gpt-4', 'user-key') + expect(key2).toBe('user-key') + }) }) describe('Model Capabilities', () => { describe('supportsTemperature', () => { - it.concurrent('should return true for models that support temperature', () => { + it('should return true for models that support temperature', () => { const supportedModels = [ 'gpt-4o', 'gpt-4.1', @@ -211,7 +195,7 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return false for models that do not support temperature', () => { + it('should return false for models that do not support temperature', () => { const unsupportedModels = [ 'unsupported-model', 'claude-sonnet-5', @@ -241,22 +225,19 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should be case insensitive', () => { + it('should be case insensitive', () => { expect(supportsTemperature('GPT-4O')).toBe(true) expect(supportsTemperature('claude-sonnet-4-5')).toBe(true) }) - it.concurrent( - 'should inherit temperature support from provider for dynamically fetched models', - () => { - expect(supportsTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(true) - expect(supportsTemperature('openrouter/openai/gpt-4')).toBe(true) - } - ) + it('should inherit temperature support from provider for dynamically fetched models', () => { + expect(supportsTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(true) + expect(supportsTemperature('openrouter/openai/gpt-4')).toBe(true) + }) }) describe('getMaxTemperature', () => { - it.concurrent('should return 2 for models with temperature range 0-2', () => { + it('should return 2 for models with temperature range 0-2', () => { const modelsRange02 = [ 'gpt-4o', 'azure/gpt-4o', @@ -276,7 +257,7 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return 1 for models with temperature range 0-1', () => { + it('should return 1 for models with temperature range 0-1', () => { const modelsRange01 = ['claude-sonnet-4-5', 'claude-opus-4-1'] for (const model of modelsRange01) { @@ -284,7 +265,7 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return 1.5 for models with temperature range 0-1.5', () => { + it('should return 1.5 for models with temperature range 0-1.5', () => { const modelsRange015 = ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] for (const model of modelsRange015) { @@ -292,7 +273,7 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return undefined for models that do not support temperature', () => { + it('should return undefined for models that do not support temperature', () => { expect(getMaxTemperature('unsupported-model')).toBeUndefined() expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined() expect(getMaxTemperature('o1')).toBeUndefined() @@ -314,22 +295,19 @@ describe('Model Capabilities', () => { expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined() }) - it.concurrent('should be case insensitive', () => { + it('should be case insensitive', () => { expect(getMaxTemperature('GPT-4O')).toBe(2) expect(getMaxTemperature('CLAUDE-SONNET-4-5')).toBe(1) }) - it.concurrent( - 'should inherit max temperature from provider for dynamically fetched models', - () => { - expect(getMaxTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(2) - expect(getMaxTemperature('openrouter/openai/gpt-4')).toBe(2) - } - ) + it('should inherit max temperature from provider for dynamically fetched models', () => { + expect(getMaxTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(2) + expect(getMaxTemperature('openrouter/openai/gpt-4')).toBe(2) + }) }) describe('supportsToolUsageControl', () => { - it.concurrent('should return true for providers that support tool usage control', () => { + it('should return true for providers that support tool usage control', () => { const supportedProviders = [ 'openai', 'azure-openai', @@ -345,20 +323,17 @@ describe('Model Capabilities', () => { } }) - it.concurrent( - 'should return false for providers that do not support tool usage control', - () => { - const unsupportedProviders = ['ollama', 'non-existent-provider'] + it('should return false for providers that do not support tool usage control', () => { + const unsupportedProviders = ['ollama', 'non-existent-provider'] - for (const provider of unsupportedProviders) { - expect(supportsToolUsageControl(provider)).toBe(false) - } + for (const provider of unsupportedProviders) { + expect(supportsToolUsageControl(provider)).toBe(false) } - ) + }) }) describe('supportsReasoningEffort', () => { - it.concurrent('should return true for models with reasoning effort capability', () => { + it('should return true for models with reasoning effort capability', () => { expect(supportsReasoningEffort('gpt-5')).toBe(true) expect(supportsReasoningEffort('gpt-5-mini')).toBe(true) expect(supportsReasoningEffort('gpt-5.1')).toBe(true) @@ -369,7 +344,7 @@ describe('Model Capabilities', () => { expect(supportsReasoningEffort('azure/o3')).toBe(true) }) - it.concurrent('should return false for models without reasoning effort capability', () => { + it('should return false for models without reasoning effort capability', () => { expect(supportsReasoningEffort('gpt-4o')).toBe(false) expect(supportsReasoningEffort('gpt-4.1')).toBe(false) expect(supportsReasoningEffort('claude-sonnet-4-5')).toBe(false) @@ -378,7 +353,7 @@ describe('Model Capabilities', () => { expect(supportsReasoningEffort('unknown-model')).toBe(false) }) - it.concurrent('should be case-insensitive', () => { + it('should be case-insensitive', () => { expect(supportsReasoningEffort('GPT-5')).toBe(true) expect(supportsReasoningEffort('O3')).toBe(true) expect(supportsReasoningEffort('GPT-4O')).toBe(false) @@ -386,7 +361,7 @@ describe('Model Capabilities', () => { }) describe('supportsVerbosity', () => { - it.concurrent('should return true for models with verbosity capability', () => { + it('should return true for models with verbosity capability', () => { expect(supportsVerbosity('gpt-5')).toBe(true) expect(supportsVerbosity('gpt-5-mini')).toBe(true) expect(supportsVerbosity('gpt-5.1')).toBe(true) @@ -394,7 +369,7 @@ describe('Model Capabilities', () => { expect(supportsVerbosity('azure/gpt-5')).toBe(true) }) - it.concurrent('should return false for models without verbosity capability', () => { + it('should return false for models without verbosity capability', () => { expect(supportsVerbosity('gpt-4o')).toBe(false) expect(supportsVerbosity('o3')).toBe(false) expect(supportsVerbosity('o4-mini')).toBe(false) @@ -402,14 +377,14 @@ describe('Model Capabilities', () => { expect(supportsVerbosity('unknown-model')).toBe(false) }) - it.concurrent('should be case-insensitive', () => { + it('should be case-insensitive', () => { expect(supportsVerbosity('GPT-5')).toBe(true) expect(supportsVerbosity('GPT-4O')).toBe(false) }) }) describe('supportsThinking', () => { - it.concurrent('should return true for models with thinking capability', () => { + it('should return true for models with thinking capability', () => { expect(supportsThinking('claude-opus-4-6')).toBe(true) expect(supportsThinking('claude-opus-4-5')).toBe(true) expect(supportsThinking('claude-sonnet-4-5')).toBe(true) @@ -418,7 +393,7 @@ describe('Model Capabilities', () => { expect(supportsThinking('gemini-3-flash-preview')).toBe(true) }) - it.concurrent('should return false for models without thinking capability', () => { + it('should return false for models without thinking capability', () => { expect(supportsThinking('gpt-4o')).toBe(false) expect(supportsThinking('gpt-5')).toBe(false) expect(supportsThinking('o3')).toBe(false) @@ -426,14 +401,14 @@ describe('Model Capabilities', () => { expect(supportsThinking('unknown-model')).toBe(false) }) - it.concurrent('should be case-insensitive', () => { + it('should be case-insensitive', () => { expect(supportsThinking('CLAUDE-OPUS-4-6')).toBe(true) expect(supportsThinking('GPT-4O')).toBe(false) }) }) describe('Model Constants', () => { - it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_2', () => { + it('should have correct models in MODELS_TEMP_RANGE_0_2', () => { expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o') expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash') expect(MODELS_TEMP_RANGE_0_2).toContain('deepseek-v3') @@ -441,13 +416,13 @@ describe('Model Capabilities', () => { expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-5') }) - it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_1', () => { + it('should have correct models in MODELS_TEMP_RANGE_0_1', () => { expect(MODELS_TEMP_RANGE_0_1).toContain('claude-sonnet-4-5') expect(MODELS_TEMP_RANGE_0_1).not.toContain('grok-3-latest') expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o') }) - it.concurrent('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => { + it('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => { expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('openai') expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('anthropic') expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('deepseek') @@ -455,20 +430,15 @@ describe('Model Capabilities', () => { expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).not.toContain('ollama') }) - it.concurrent( - 'should combine both temperature ranges in MODELS_WITH_TEMPERATURE_SUPPORT', - () => { - expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe( - MODELS_TEMP_RANGE_0_2.length + - MODELS_TEMP_RANGE_0_15.length + - MODELS_TEMP_RANGE_0_1.length - ) - expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') - expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-5') - } - ) + it('should combine both temperature ranges in MODELS_WITH_TEMPERATURE_SUPPORT', () => { + expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe( + MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_15.length + MODELS_TEMP_RANGE_0_1.length + ) + expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') + expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-5') + }) - it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => { + it('should have correct models in MODELS_WITH_REASONING_EFFORT', () => { expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.1') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-codex') @@ -499,7 +469,7 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-5') }) - it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => { + it('should have correct models in MODELS_WITH_VERBOSITY', () => { expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.1') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-codex') @@ -528,7 +498,7 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-5') }) - it.concurrent('should have correct models in MODELS_WITH_THINKING', () => { + it('should have correct models in MODELS_WITH_THINKING', () => { expect(MODELS_WITH_THINKING).toContain('claude-opus-4-6') expect(MODELS_WITH_THINKING).toContain('claude-opus-4-5') expect(MODELS_WITH_THINKING).toContain('claude-opus-4-1') @@ -543,7 +513,7 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_THINKING).not.toContain('o3') }) - it.concurrent('should have GPT-5 models in both reasoning effort and verbosity arrays', () => { + it('should have GPT-5 models in both reasoning effort and verbosity arrays', () => { const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter( (m) => m.includes('gpt-5') && @@ -575,7 +545,7 @@ describe('Model Capabilities', () => { }) }) describe('Reasoning Effort Values Per Model', () => { - it.concurrent('should return correct values for GPT-5.2', () => { + it('should return correct values for GPT-5.2', () => { const values = getReasoningEffortValuesForModel('gpt-5.2') expect(values).toBeDefined() expect(values).toContain('none') @@ -586,7 +556,7 @@ describe('Model Capabilities', () => { expect(values).not.toContain('minimal') }) - it.concurrent('should return correct values for GPT-5', () => { + it('should return correct values for GPT-5', () => { const values = getReasoningEffortValuesForModel('gpt-5') expect(values).toBeDefined() expect(values).toContain('minimal') @@ -595,7 +565,7 @@ describe('Model Capabilities', () => { expect(values).toContain('high') }) - it.concurrent('should return correct values for o-series models', () => { + it('should return correct values for o-series models', () => { for (const model of ['o1', 'o3', 'o4-mini']) { const values = getReasoningEffortValuesForModel(model) expect(values).toBeDefined() @@ -607,13 +577,13 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return null for non-reasoning models', () => { + it('should return null for non-reasoning models', () => { expect(getReasoningEffortValuesForModel('gpt-4o')).toBeNull() expect(getReasoningEffortValuesForModel('claude-sonnet-4-5')).toBeNull() expect(getReasoningEffortValuesForModel('gemini-2.5-flash')).toBeNull() }) - it.concurrent('should return correct values for Azure GPT-5.2', () => { + it('should return correct values for Azure GPT-5.2', () => { const values = getReasoningEffortValuesForModel('azure/gpt-5.2') expect(values).toBeDefined() expect(values).not.toContain('minimal') @@ -624,7 +594,7 @@ describe('Model Capabilities', () => { }) describe('Verbosity Values Per Model', () => { - it.concurrent('should return correct values for GPT-5 family', () => { + it('should return correct values for GPT-5 family', () => { for (const model of ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano']) { const values = getVerbosityValuesForModel(model) expect(values).toBeDefined() @@ -634,20 +604,20 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return null for o-series models', () => { + it('should return null for o-series models', () => { expect(getVerbosityValuesForModel('o1')).toBeNull() expect(getVerbosityValuesForModel('o3')).toBeNull() expect(getVerbosityValuesForModel('o4-mini')).toBeNull() }) - it.concurrent('should return null for non-reasoning models', () => { + it('should return null for non-reasoning models', () => { expect(getVerbosityValuesForModel('gpt-4o')).toBeNull() expect(getVerbosityValuesForModel('claude-sonnet-4-5')).toBeNull() }) }) describe('Thinking Levels Per Model', () => { - it.concurrent('should return correct levels for Claude Opus 4.6 (adaptive)', () => { + it('should return correct levels for Claude Opus 4.6 (adaptive)', () => { const levels = getThinkingLevelsForModel('claude-opus-4-6') expect(levels).toBeDefined() expect(levels).toContain('low') @@ -656,7 +626,7 @@ describe('Model Capabilities', () => { expect(levels).toContain('max') }) - it.concurrent('should return correct levels for other Claude models (budget_tokens)', () => { + it('should return correct levels for other Claude models (budget_tokens)', () => { for (const model of ['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5']) { const levels = getThinkingLevelsForModel(model) expect(levels).toBeDefined() @@ -667,7 +637,7 @@ describe('Model Capabilities', () => { } }) - it.concurrent('should return correct levels for Gemini 3 models', () => { + it('should return correct levels for Gemini 3 models', () => { const flashLevels = getThinkingLevelsForModel('gemini-3-flash-preview') expect(flashLevels).toBeDefined() expect(flashLevels).toContain('minimal') @@ -676,7 +646,7 @@ describe('Model Capabilities', () => { expect(flashLevels).toContain('high') }) - it.concurrent('should return correct levels for Claude Haiku 4.5', () => { + it('should return correct levels for Claude Haiku 4.5', () => { const levels = getThinkingLevelsForModel('claude-haiku-4-5') expect(levels).toBeDefined() expect(levels).toContain('low') @@ -684,7 +654,7 @@ describe('Model Capabilities', () => { expect(levels).toContain('high') }) - it.concurrent('should return null for non-thinking models', () => { + it('should return null for non-thinking models', () => { expect(getThinkingLevelsForModel('gpt-4o')).toBeNull() expect(getThinkingLevelsForModel('gpt-5')).toBeNull() expect(getThinkingLevelsForModel('o3')).toBeNull() @@ -694,65 +664,65 @@ describe('Model Capabilities', () => { describe('Max Output Tokens', () => { describe('getMaxOutputTokensForModel', () => { - it.concurrent('should return published max for OpenAI GPT-4o', () => { + it('should return published max for OpenAI GPT-4o', () => { expect(getMaxOutputTokensForModel('gpt-4o')).toBe(16384) }) - it.concurrent('should return published max for OpenAI GPT-5.1', () => { + it('should return published max for OpenAI GPT-5.1', () => { expect(getMaxOutputTokensForModel('gpt-5.1')).toBe(128000) }) - it.concurrent('should return published max for OpenAI GPT-5 Chat', () => { + it('should return published max for OpenAI GPT-5 Chat', () => { expect(getMaxOutputTokensForModel('gpt-5-chat-latest')).toBe(16384) }) - it.concurrent('should return published max for OpenAI o1', () => { + it('should return published max for OpenAI o1', () => { expect(getMaxOutputTokensForModel('o1')).toBe(100000) }) - it.concurrent('should return updated max for Claude Sonnet 4.6', () => { + it('should return updated max for Claude Sonnet 4.6', () => { expect(getMaxOutputTokensForModel('claude-sonnet-4-6')).toBe(128000) }) - it.concurrent('should return published max for Gemini 2.5 Pro', () => { + it('should return published max for Gemini 2.5 Pro', () => { expect(getMaxOutputTokensForModel('gemini-2.5-pro')).toBe(65536) }) - it.concurrent('should return published max for Azure GPT-5.2', () => { + it('should return published max for Azure GPT-5.2', () => { expect(getMaxOutputTokensForModel('azure/gpt-5.2')).toBe(128000) }) - it.concurrent('should return standard default for models without maxOutputTokens', () => { + it('should return standard default for models without maxOutputTokens', () => { expect(getMaxOutputTokensForModel('deepseek-reasoner')).toBe(4096) expect(getMaxOutputTokensForModel('grok-4-latest')).toBe(4096) }) - it.concurrent('should return published max for Bedrock Claude Opus 4.1', () => { + it('should return published max for Bedrock Claude Opus 4.1', () => { expect(getMaxOutputTokensForModel('bedrock/anthropic.claude-opus-4-1-20250805-v1:0')).toBe( 32000 ) }) - it.concurrent('should return correct max for Claude Opus 4.6', () => { + it('should return correct max for Claude Opus 4.6', () => { expect(getMaxOutputTokensForModel('claude-opus-4-6')).toBe(128000) }) - it.concurrent('should return correct max for Claude Sonnet 4.5', () => { + it('should return correct max for Claude Sonnet 4.5', () => { expect(getMaxOutputTokensForModel('claude-sonnet-4-5')).toBe(64000) }) - it.concurrent('should return correct max for Claude Opus 4.1', () => { + it('should return correct max for Claude Opus 4.1', () => { expect(getMaxOutputTokensForModel('claude-opus-4-1')).toBe(32000) }) - it.concurrent('should return standard default for unknown models', () => { + it('should return standard default for unknown models', () => { expect(getMaxOutputTokensForModel('unknown-model')).toBe(4096) }) }) }) describe('Model Pricing Validation', () => { - it.concurrent('should have correct pricing for key Anthropic models', () => { + it('should have correct pricing for key Anthropic models', () => { const opus46 = getModelPricing('claude-opus-4-6') expect(opus46).toBeDefined() expect(opus46.input).toBe(5.0) @@ -764,7 +734,7 @@ describe('Model Pricing Validation', () => { expect(sonnet45.output).toBe(15.0) }) - it.concurrent('should have correct pricing for key OpenAI models', () => { + it('should have correct pricing for key OpenAI models', () => { const gpt4o = getModelPricing('gpt-4o') expect(gpt4o).toBeDefined() expect(gpt4o.input).toBe(2.5) @@ -776,20 +746,20 @@ describe('Model Pricing Validation', () => { expect(o3.output).toBe(8.0) }) - it.concurrent('should have correct pricing for Azure OpenAI o3', () => { + it('should have correct pricing for Azure OpenAI o3', () => { const azureO3 = getModelPricing('azure/o3') expect(azureO3).toBeDefined() expect(azureO3.input).toBe(2.0) expect(azureO3.output).toBe(8.0) }) - it.concurrent('should return null for unknown models', () => { + it('should return null for unknown models', () => { expect(getModelPricing('unknown-model')).toBeNull() }) }) describe('Context Window Validation', () => { - it.concurrent('should have correct context windows for key models', () => { + it('should have correct context windows for key models', () => { const allModels = getAllModels() expect(allModels).toContain('gpt-5-chat-latest') @@ -801,7 +771,7 @@ describe('Context Window Validation', () => { describe('Cost Calculation', () => { describe('calculateCost', () => { - it.concurrent('should calculate cost correctly for known models', () => { + it('should calculate cost correctly for known models', () => { const result = calculateCost('gpt-4o', 1000, 500, false) expect(result.input).toBeGreaterThan(0) @@ -811,7 +781,7 @@ describe('Cost Calculation', () => { expect(result.pricing.input).toBe(2.5) }) - it.concurrent('should handle cached input pricing when enabled', () => { + it('should handle cached input pricing when enabled', () => { const regularCost = calculateCost('gpt-4o', 1000, 500, false) const cachedCost = calculateCost('gpt-4o', 1000, 500, true) @@ -819,7 +789,7 @@ describe('Cost Calculation', () => { expect(cachedCost.output).toBe(regularCost.output) }) - it.concurrent('should return default pricing for unknown models', () => { + it('should return default pricing for unknown models', () => { const result = calculateCost('unknown-model', 1000, 500, false) expect(result.input).toBe(0) @@ -828,7 +798,7 @@ describe('Cost Calculation', () => { expect(result.pricing.input).toBe(1.0) }) - it.concurrent('should handle zero tokens', () => { + it('should handle zero tokens', () => { const result = calculateCost('gpt-4o', 0, 0, false) expect(result.input).toBe(0) @@ -838,26 +808,26 @@ describe('Cost Calculation', () => { }) describe('formatCost', () => { - it.concurrent('should format dollar amounts as credits', () => { + it('should format dollar amounts as credits', () => { expect(formatCost(1.234)).toBe('247 credits') expect(formatCost(10.567)).toBe('2,113 credits') }) - it.concurrent('should show <1 credit for very small costs', () => { + it('should show <1 credit for very small costs', () => { expect(formatCost(0.0024)).toBe('<1 credit') expect(formatCost(0.001)).toBe('<1 credit') }) - it.concurrent('should show credit count for small costs that round to at least 1', () => { + it('should show credit count for small costs that round to at least 1', () => { expect(formatCost(0.0234)).toBe('5 credits') expect(formatCost(0.1567)).toBe('31 credits') }) - it.concurrent('should handle zero cost', () => { + it('should handle zero cost', () => { expect(formatCost(0)).toBe('0 credits') }) - it.concurrent('should handle undefined/null costs', () => { + it('should handle undefined/null costs', () => { expect(formatCost(undefined as any)).toBe('—') expect(formatCost(null as any)).toBe('—') }) @@ -865,7 +835,7 @@ describe('Cost Calculation', () => { }) describe('getHostedModels', () => { - it.concurrent('should return OpenAI, Anthropic, Google, and xAI models as hosted', () => { + it('should return OpenAI, Anthropic, Google, and xAI models as hosted', () => { const hostedModels = getHostedModels() expect(hostedModels).toContain('gpt-4o') @@ -882,7 +852,7 @@ describe('getHostedModels', () => { expect(hostedModels).not.toContain('deepseek-v3') }) - it.concurrent('should return an array of strings', () => { + it('should return an array of strings', () => { const hostedModels = getHostedModels() expect(Array.isArray(hostedModels)).toBe(true) @@ -894,7 +864,7 @@ describe('getHostedModels', () => { }) describe('shouldBillModelUsage', () => { - it.concurrent('should return true for exact matches of hosted models', () => { + it('should return true for exact matches of hosted models', () => { expect(shouldBillModelUsage('gpt-4o')).toBe(true) expect(shouldBillModelUsage('o1')).toBe(true) @@ -907,25 +877,25 @@ describe('shouldBillModelUsage', () => { expect(shouldBillModelUsage('grok-4.5')).toBe(true) }) - it.concurrent('should return false for non-hosted models', () => { + it('should return false for non-hosted models', () => { expect(shouldBillModelUsage('deepseek-v3')).toBe(false) expect(shouldBillModelUsage('unknown-model')).toBe(false) }) - it.concurrent('should return false for versioned model names not in hosted list', () => { + it('should return false for versioned model names not in hosted list', () => { expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false) expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false) expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false) }) - it.concurrent('should be case insensitive', () => { + it('should be case insensitive', () => { expect(shouldBillModelUsage('GPT-4O')).toBe(true) expect(shouldBillModelUsage('Claude-Sonnet-4-5')).toBe(true) expect(shouldBillModelUsage('GEMINI-2.5-PRO')).toBe(true) }) - it.concurrent('should not match partial model names', () => { + it('should not match partial model names', () => { expect(shouldBillModelUsage('gpt-4')).toBe(false) expect(shouldBillModelUsage('claude-sonnet')).toBe(false) expect(shouldBillModelUsage('gemini')).toBe(false) @@ -934,30 +904,30 @@ describe('shouldBillModelUsage', () => { describe('Provider Management', () => { describe('getProviderFromModel', () => { - it.concurrent('should return correct provider for known models', () => { + it('should return correct provider for known models', () => { expect(getProviderFromModel('gpt-4o')).toBe('openai') expect(getProviderFromModel('claude-sonnet-4-5')).toBe('anthropic') expect(getProviderFromModel('gemini-2.5-pro')).toBe('google') expect(getProviderFromModel('azure/gpt-4o')).toBe('azure-openai') }) - it.concurrent('should use model patterns for pattern matching', () => { + it('should use model patterns for pattern matching', () => { expect(getProviderFromModel('gpt-5-custom')).toBe('openai') expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') }) - it.concurrent('should default to ollama for unknown models', () => { + it('should default to ollama for unknown models', () => { expect(getProviderFromModel('unknown-model')).toBe('ollama') }) - it.concurrent('should be case insensitive', () => { + it('should be case insensitive', () => { expect(getProviderFromModel('GPT-4O')).toBe('openai') expect(getProviderFromModel('CLAUDE-SONNET-4-0')).toBe('anthropic') }) }) describe('getProvider', () => { - it.concurrent('should return provider config for valid provider IDs', () => { + it('should return provider config for valid provider IDs', () => { const openaiProvider = getProvider('openai') expect(openaiProvider).toBeDefined() expect(openaiProvider?.id).toBe('openai') @@ -968,19 +938,19 @@ describe('Provider Management', () => { expect(anthropicProvider?.id).toBe('anthropic') }) - it.concurrent('should handle provider/service format', () => { + it('should handle provider/service format', () => { const provider = getProvider('openai/chat') expect(provider).toBeDefined() expect(provider?.id).toBe('openai') }) - it.concurrent('should return undefined for invalid provider IDs', () => { + it('should return undefined for invalid provider IDs', () => { expect(getProvider('nonexistent')).toBeUndefined() }) }) describe('getProviderConfigFromModel', () => { - it.concurrent('should return provider config for model', () => { + it('should return provider config for model', () => { const config = getProviderConfigFromModel('gpt-4o') expect(config).toBeDefined() expect(config?.id).toBe('openai') @@ -992,7 +962,7 @@ describe('Provider Management', () => { }) describe('getAllModels', () => { - it.concurrent('should return all models from all providers', () => { + it('should return all models from all providers', () => { const allModels = getAllModels() expect(Array.isArray(allModels)).toBe(true) expect(allModels.length).toBeGreaterThan(0) @@ -1004,7 +974,7 @@ describe('Provider Management', () => { }) describe('getAllProviderIds', () => { - it.concurrent('should return all provider IDs', () => { + it('should return all provider IDs', () => { const providerIds = getAllProviderIds() expect(Array.isArray(providerIds)).toBe(true) expect(providerIds).toContain('openai') @@ -1015,7 +985,7 @@ describe('Provider Management', () => { }) describe('getProviderModels', () => { - it.concurrent('should return models for specific providers', () => { + it('should return models for specific providers', () => { const openaiModels = getProviderModels('openai') expect(Array.isArray(openaiModels)).toBe(true) expect(openaiModels).toContain('gpt-4o') @@ -1026,14 +996,14 @@ describe('Provider Management', () => { expect(anthropicModels).toContain('claude-opus-4-1') }) - it.concurrent('should return empty array for unknown providers', () => { + it('should return empty array for unknown providers', () => { const unknownModels = getProviderModels('unknown' as any) expect(unknownModels).toEqual([]) }) }) describe('getBaseModelProviders and getAllModelProviders', () => { - it.concurrent('should return model to provider mapping', () => { + it('should return model to provider mapping', () => { const allProviders = getAllModelProviders() expect(typeof allProviders).toBe('object') expect(allProviders['gpt-4o']).toBe('openai') @@ -1045,7 +1015,7 @@ describe('Provider Management', () => { }) describe('updateOllamaProviderModels', () => { - it.concurrent('should update ollama models', () => { + it('should update ollama models', () => { const mockModels = ['llama2', 'codellama', 'mistral'] expect(() => updateOllamaProviderModels(mockModels)).not.toThrow() @@ -1058,19 +1028,19 @@ describe('Provider Management', () => { describe('JSON and Structured Output', () => { describe('extractAndParseJSON', () => { - it.concurrent('should extract and parse valid JSON', () => { + it('should extract and parse valid JSON', () => { const content = 'Some text before ```json\n{"key": "value"}\n``` some text after' const result = extractAndParseJSON(content) expect(result).toEqual({ key: 'value' }) }) - it.concurrent('should extract JSON without code blocks', () => { + it('should extract JSON without code blocks', () => { const content = 'Text before {"name": "test", "value": 42} text after' const result = extractAndParseJSON(content) expect(result).toEqual({ name: 'test', value: 42 }) }) - it.concurrent('should handle nested objects', () => { + it('should handle nested objects', () => { const content = '{"user": {"name": "John", "age": 30}, "active": true}' const result = extractAndParseJSON(content) expect(result).toEqual({ @@ -1079,24 +1049,24 @@ describe('JSON and Structured Output', () => { }) }) - it.concurrent('should clean up common JSON issues', () => { + it('should clean up common JSON issues', () => { const content = '{\n "key": "value",\n "number": 42,\n}' const result = extractAndParseJSON(content) expect(result).toEqual({ key: 'value', number: 42 }) }) - it.concurrent('should throw error for content without JSON', () => { + it('should throw error for content without JSON', () => { expect(() => extractAndParseJSON('No JSON here')).toThrow('No JSON object found in content') }) - it.concurrent('should throw error for invalid JSON', () => { + it('should throw error for invalid JSON', () => { const invalidJson = '{"key": invalid, "broken": }' expect(() => extractAndParseJSON(invalidJson)).toThrow('Failed to parse JSON after cleanup') }) }) describe('generateStructuredOutputInstructions', () => { - it.concurrent('should return empty string for JSON Schema format', () => { + it('should return empty string for JSON Schema format', () => { const schemaFormat = { schema: { type: 'object', @@ -1106,7 +1076,7 @@ describe('JSON and Structured Output', () => { expect(generateStructuredOutputInstructions(schemaFormat)).toBe('') }) - it.concurrent('should return empty string for object type with properties', () => { + it('should return empty string for object type with properties', () => { const objectFormat = { type: 'object', properties: { key: { type: 'string' } }, @@ -1114,7 +1084,7 @@ describe('JSON and Structured Output', () => { expect(generateStructuredOutputInstructions(objectFormat)).toBe('') }) - it.concurrent('should generate instructions for legacy fields format', () => { + it('should generate instructions for legacy fields format', () => { const fieldsFormat = { fields: [ { name: 'score', type: 'number', description: 'A score from 1-10' }, @@ -1129,7 +1099,7 @@ describe('JSON and Structured Output', () => { expect(result).toContain('A score from 1-10') }) - it.concurrent('should handle object fields with properties', () => { + it('should handle object fields with properties', () => { const fieldsFormat = { fields: [ { @@ -1150,7 +1120,7 @@ describe('JSON and Structured Output', () => { expect(result).toContain('count') }) - it.concurrent('should return empty string for missing fields', () => { + it('should return empty string for missing fields', () => { expect(generateStructuredOutputInstructions({})).toBe('') expect(generateStructuredOutputInstructions(null)).toBe('') expect(generateStructuredOutputInstructions({ fields: null })).toBe('') @@ -1170,7 +1140,7 @@ describe('Tool Management', () => { mockLogger.info.mockClear() }) - it.concurrent('should return early for no tools', () => { + it('should return early for no tools', () => { const result = prepareToolsWithUsageControl(undefined, undefined, mockLogger) expect(result.tools).toBeUndefined() @@ -1179,7 +1149,7 @@ describe('Tool Management', () => { expect(result.forcedTools).toEqual([]) }) - it.concurrent('should filter out tools with usageControl="none"', () => { + it('should filter out tools with usageControl="none"', () => { const tools = [ { function: { name: 'tool1' } }, { function: { name: 'tool2' } }, @@ -1199,7 +1169,7 @@ describe('Tool Management', () => { expect(mockLogger.info).toHaveBeenCalledWith("Filtered out 1 tools with usageControl='none'") }) - it.concurrent('should set toolChoice for forced tools (OpenAI format)', () => { + it('should set toolChoice for forced tools (OpenAI format)', () => { const tools = [{ function: { name: 'forcedTool' } }] const providerTools = [{ id: 'forcedTool', usageControl: 'force' }] @@ -1211,7 +1181,7 @@ describe('Tool Management', () => { }) }) - it.concurrent('should set toolChoice for forced tools (Anthropic format)', () => { + it('should set toolChoice for forced tools (Anthropic format)', () => { const tools = [{ function: { name: 'forcedTool' } }] const providerTools = [{ id: 'forcedTool', usageControl: 'force' }] @@ -1223,7 +1193,7 @@ describe('Tool Management', () => { }) }) - it.concurrent('should set toolConfig for Google format', () => { + it('should set toolConfig for Google format', () => { const tools = [{ function: { name: 'forcedTool' } }] const providerTools = [{ id: 'forcedTool', usageControl: 'force' }] @@ -1237,7 +1207,7 @@ describe('Tool Management', () => { }) }) - it.concurrent('should return empty when all tools are filtered', () => { + it('should return empty when all tools are filtered', () => { const tools = [{ function: { name: 'tool1' } }] const providerTools = [{ id: 'tool1', usageControl: 'none' }] @@ -1248,7 +1218,7 @@ describe('Tool Management', () => { expect(result.hasFilteredTools).toBe(true) }) - it.concurrent('should default to auto when no forced tools', () => { + it('should default to auto when no forced tools', () => { const tools = [{ function: { name: 'tool1' } }] const providerTools = [{ id: 'tool1', usageControl: 'auto' }] @@ -1261,7 +1231,7 @@ describe('Tool Management', () => { describe('prepareToolExecution', () => { describe('basic parameter merging', () => { - it.concurrent('should merge LLM args with user params', () => { + it('should merge LLM args with user params', () => { const tool = { params: { apiKey: 'user-key', channel: '#general' }, } @@ -1275,7 +1245,7 @@ describe('prepareToolExecution', () => { expect(toolParams.message).toBe('Hello world') }) - it.concurrent('should filter out empty string user params', () => { + it('should filter out empty string user params', () => { const tool = { params: { apiKey: 'user-key', channel: '' }, } @@ -1304,24 +1274,21 @@ describe('prepareToolExecution', () => { payerSubscription: null, } - it.concurrent( - 'should include billingAttribution in _context when the request carries it', - () => { - const tool = { params: {} } - const request = { - workflowId: 'wf-123', - workspaceId: 'workspace-1', - userId: 'user-1', - billingAttribution, - } + it('should include billingAttribution in _context when the request carries it', () => { + const tool = { params: {} } + const request = { + workflowId: 'wf-123', + workspaceId: 'workspace-1', + userId: 'user-1', + billingAttribution, + } - const { executionParams } = prepareToolExecution(tool, {}, request) + const { executionParams } = prepareToolExecution(tool, {}, request) - expect(executionParams._context.billingAttribution).toEqual(billingAttribution) - } - ) + expect(executionParams._context.billingAttribution).toEqual(billingAttribution) + }) - it.concurrent('should omit billingAttribution from _context when the request lacks it', () => { + it('should omit billingAttribution from _context when the request lacks it', () => { const tool = { params: {} } const request = { workflowId: 'wf-123', workspaceId: 'workspace-1' } @@ -1331,7 +1298,7 @@ describe('prepareToolExecution', () => { expect(executionParams._context).not.toHaveProperty('billingAttribution') }) - it.concurrent('should carry billingAttribution even when the request has no workflowId', () => { + it('should carry billingAttribution even when the request has no workflowId', () => { const tool = { params: {} } const request = { workspaceId: 'workspace-1', billingAttribution } @@ -1342,7 +1309,7 @@ describe('prepareToolExecution', () => { expect(executionParams._context).not.toHaveProperty('workflowId') }) - it.concurrent('should not build _context when there is no workflowId or attribution', () => { + it('should not build _context when there is no workflowId or attribution', () => { const tool = { params: {} } const { executionParams } = prepareToolExecution(tool, {}, { workspaceId: 'workspace-1' }) @@ -1352,7 +1319,7 @@ describe('prepareToolExecution', () => { }) describe('inputMapping deep merge for workflow tools', () => { - it.concurrent('should deep merge inputMapping when user provides empty object', () => { + it('should deep merge inputMapping when user provides empty object', () => { const tool = { params: { workflowId: 'child-workflow-123', @@ -1370,7 +1337,7 @@ describe('prepareToolExecution', () => { expect(toolParams.workflowId).toBe('child-workflow-123') }) - it.concurrent('should deep merge inputMapping with partial user values', () => { + it('should deep merge inputMapping with partial user values', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1391,7 +1358,7 @@ describe('prepareToolExecution', () => { }) }) - it.concurrent('should preserve non-empty user inputMapping values', () => { + it('should preserve non-empty user inputMapping values', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1412,7 +1379,7 @@ describe('prepareToolExecution', () => { }) }) - it.concurrent('should handle inputMapping as object (not JSON string)', () => { + it('should handle inputMapping as object (not JSON string)', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1433,7 +1400,7 @@ describe('prepareToolExecution', () => { }) }) - it.concurrent('should use LLM inputMapping when user does not provide it', () => { + it('should use LLM inputMapping when user does not provide it', () => { const tool = { params: { workflowId: 'child-workflow' }, } @@ -1447,7 +1414,7 @@ describe('prepareToolExecution', () => { expect(toolParams.inputMapping).toEqual({ query: 'llm-search', limit: 10 }) }) - it.concurrent('should use user inputMapping when LLM does not provide it', () => { + it('should use user inputMapping when LLM does not provide it', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1462,7 +1429,7 @@ describe('prepareToolExecution', () => { expect(toolParams.inputMapping).toEqual({ query: 'user-search' }) }) - it.concurrent('should handle invalid JSON in user inputMapping gracefully', () => { + it('should handle invalid JSON in user inputMapping gracefully', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1479,7 +1446,7 @@ describe('prepareToolExecution', () => { expect(toolParams.inputMapping).toEqual({ query: 'llm-search' }) }) - it.concurrent('should not affect other parameters - normal override behavior', () => { + it('should not affect other parameters - normal override behavior', () => { const tool = { params: { apiKey: 'user-key', channel: '#general' }, } @@ -1493,7 +1460,7 @@ describe('prepareToolExecution', () => { expect(toolParams.message).toBe('Hello') }) - it.concurrent('should preserve 0 and false as valid user values in inputMapping', () => { + it('should preserve 0 and false as valid user values in inputMapping', () => { const tool = { params: { workflowId: 'child-workflow', @@ -1516,7 +1483,7 @@ describe('prepareToolExecution', () => { }) describe('execution params context', () => { - it.concurrent('should include workflow context in executionParams', () => { + it('should include workflow context in executionParams', () => { const tool = { params: { message: 'test' } } const llmArgs = {} const request = { @@ -1536,7 +1503,7 @@ describe('prepareToolExecution', () => { }) }) - it.concurrent('should include environment and workflow variables', () => { + it('should include environment and workflow variables', () => { const tool = { params: {} } const llmArgs = {} const request = { @@ -1554,27 +1521,27 @@ describe('prepareToolExecution', () => { describe('Provider/Model Blacklist', () => { describe('isProviderBlacklisted', () => { - it.concurrent('should return false when no providers are blacklisted', () => { + it('should return false when no providers are blacklisted', () => { expect(isProviderBlacklisted('openai')).toBe(false) expect(isProviderBlacklisted('anthropic')).toBe(false) }) }) describe('filterBlacklistedModels', () => { - it.concurrent('should return all models when no blacklist is set', () => { + it('should return all models when no blacklist is set', () => { const models = ['gpt-4o', 'claude-sonnet-4-5', 'gemini-2.5-pro'] const result = filterBlacklistedModels(models) expect(result).toEqual(models) }) - it.concurrent('should return empty array for empty input', () => { + it('should return empty array for empty input', () => { const result = filterBlacklistedModels([]) expect(result).toEqual([]) }) }) describe('getBaseModelProviders blacklist filtering', () => { - it.concurrent('should return providers when no blacklist is set', () => { + it('should return providers when no blacklist is set', () => { const providers = getBaseModelProviders() expect(Object.keys(providers).length).toBeGreaterThan(0) expect(providers['gpt-4o']).toBe('openai') @@ -1583,12 +1550,12 @@ describe('Provider/Model Blacklist', () => { }) describe('getProviderFromModel execution-time enforcement', () => { - it.concurrent('should return provider for non-blacklisted models', () => { + it('should return provider for non-blacklisted models', () => { expect(getProviderFromModel('gpt-4o')).toBe('openai') expect(getProviderFromModel('claude-sonnet-4-5')).toBe('anthropic') }) - it.concurrent('should be case insensitive', () => { + it('should be case insensitive', () => { expect(getProviderFromModel('GPT-4O')).toBe('openai') expect(getProviderFromModel('CLAUDE-SONNET-4-5')).toBe('anthropic') }) diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index 5df9984cf10..929eed52aff 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -14,14 +14,15 @@ import { inputValidationMock, inputValidationMockFns, type MockFetchResponse, + resetEnvFlagsMock, + setEnvFlags, } from '@sim/testing' import { sleep } from '@sim/utils/helpers' -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution' // Hoisted mock state - these are available to vi.mock factories const { - mockIsHosted, mockEnv, mockGetBYOKKey, mockGetToolAsync, @@ -33,7 +34,6 @@ const { mockResolveWorkspaceFileReference, mockGetEffectiveDecryptedEnv, } = vi.hoisted(() => ({ - mockIsHosted: { value: false }, mockEnv: { NEXT_PUBLIC_APP_URL: 'http://localhost:3000' } as Record, mockGetBYOKKey: vi.fn(), mockGetToolAsync: vi.fn(), @@ -53,16 +53,6 @@ const { const mockSecureFetchWithPinnedIP = inputValidationMockFns.mockSecureFetchWithPinnedIP const mockValidateUrlWithDNS = inputValidationMockFns.mockValidateUrlWithDNS -// Mock feature flags -vi.mock('@/lib/core/config/env-flags', () => ({ - get isHosted() { - return mockIsHosted.value - }, - isProd: false, - isDev: true, - isTest: true, -})) - // Mock env config to control hosted key availability vi.mock('@/lib/core/config/env', () => ({ env: new Proxy({} as Record, { @@ -480,6 +470,12 @@ function setupEnvVars(variables: Record) { } } +beforeAll(() => { + setEnvFlags({ isDev: true }) +}) + +afterAll(resetEnvFlagsMock) + describe('Tools Registry', () => { it('should include all expected built-in tools', () => { expect(tools.http_request).toBeDefined() @@ -2553,7 +2549,7 @@ describe('Rate Limiting and Retry Logic', () => { NEXT_PUBLIC_APP_URL: 'http://localhost:3000', }) vi.clearAllMocks() - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) mockEnv.TEST_HOSTED_KEY = 'test-hosted-api-key' mockGetBYOKKey.mockResolvedValue(null) // Set up throttler mock defaults @@ -2571,7 +2567,7 @@ describe('Rate Limiting and Retry Logic', () => { vi.useRealTimers() vi.resetAllMocks() cleanupEnvVars() - mockIsHosted.value = false + setEnvFlags({ isHosted: false }) mockEnv.TEST_HOSTED_KEY = undefined }) @@ -2937,7 +2933,7 @@ describe('Cost Field Handling', () => { NEXT_PUBLIC_APP_URL: 'http://localhost:3000', }) vi.clearAllMocks() - mockIsHosted.value = true + setEnvFlags({ isHosted: true }) mockEnv.TEST_HOSTED_KEY = 'test-hosted-api-key' mockGetBYOKKey.mockResolvedValue(null) // Set up throttler mock defaults @@ -2954,7 +2950,7 @@ describe('Cost Field Handling', () => { afterEach(() => { vi.resetAllMocks() cleanupEnvVars() - mockIsHosted.value = false + setEnvFlags({ isHosted: false }) mockEnv.TEST_HOSTED_KEY = undefined }) @@ -3020,7 +3016,7 @@ describe('Cost Field Handling', () => { }) it('should not add cost when not using hosted key', async () => { - mockIsHosted.value = false + setEnvFlags({ isHosted: false }) const mockTool = { id: 'test_no_hosted_cost', diff --git a/apps/sim/tools/supabase/utils.test.ts b/apps/sim/tools/supabase/utils.test.ts index b12e98ac421..8ee7c963cf5 100644 --- a/apps/sim/tools/supabase/utils.test.ts +++ b/apps/sim/tools/supabase/utils.test.ts @@ -1,11 +1,7 @@ /** * @vitest-environment node */ -import { envFlagsMock } from '@sim/testing' -import { describe, expect, it, vi } from 'vitest' - -vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - +import { describe, expect, it } from 'vitest' import { supabaseBaseUrl } from '@/tools/supabase/utils' describe('supabaseBaseUrl', () => { diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts index 1213b48efc8..4abf8e34bb2 100644 --- a/apps/sim/vitest.setup.ts +++ b/apps/sim/vitest.setup.ts @@ -2,6 +2,7 @@ import { authMock, databaseMock, drizzleOrmMock, + envFlagsMock, hybridAuthMock, loggerMock, requestUtilsMock, @@ -25,6 +26,7 @@ vi.mock('@sim/platform-authz/workflow', () => workflowAuthzMock) vi.mock('@/lib/auth', () => authMock) vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) vi.mock('@/stores/console/store', () => ({ useConsoleStore: { diff --git a/packages/testing/src/mocks/env-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts index 590a5b9fde6..ee6557a4245 100644 --- a/packages/testing/src/mocks/env-flags.mock.ts +++ b/packages/testing/src/mocks/env-flags.mock.ts @@ -1,25 +1,66 @@ import { vi } from 'vitest' /** - * Static mock module for `@/lib/core/config/env-flags`. - * All boolean flags default to `false` for safe test isolation. - * - * @example - * ```ts - * vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) - * ``` + * Mutable value-export state for the shared `@/lib/core/config/env-flags` mock. + * Defaults mirror the real module evaluated under the vitest environment + * (NODE_ENV=test, no feature env vars set): only `isTest` and + * `isEmailPasswordEnabled` are true. */ -export const envFlagsMock = { +export interface EnvFlagsMockState { + isProd: boolean + isDev: boolean + isTest: boolean + isHosted: boolean + isCopilotBillingAttributionV1Enabled: boolean + isCopilotBillingProtocolRequired: boolean + isBillingEnabled: boolean + isEmailVerificationEnabled: boolean + isAuthDisabled: boolean + isPrivateDatabaseHostsAllowed: boolean + isRegistrationDisabled: boolean + isEmailPasswordEnabled: boolean + isSignupMxValidationEnabled: boolean + isAppConfigEnabled: boolean + isTriggerDevEnabled: boolean + isSsoEnabled: boolean + isAccessControlEnabled: boolean + isOrganizationsEnabled: boolean + isInboxEnabled: boolean + isWhitelabelingEnabled: boolean + isAuditLogsEnabled: boolean + isDataRetentionEnabled: boolean + isDataDrainsEnabled: boolean + isForkingEnabled: boolean + isE2bEnabled: boolean + isE2BDocEnabled: boolean + isOllamaConfigured: boolean + isAzureConfigured: boolean + isCohereConfigured: boolean + isInvitationsDisabled: boolean + isPublicApiDisabled: boolean + isGoogleAuthDisabled: boolean + isGithubAuthDisabled: boolean + isMicrosoftAuthDisabled: boolean + isEmailSignupDisabled: boolean + isReactGrabEnabled: boolean + isReactScanEnabled: boolean +} + +const defaultEnvFlagsState: EnvFlagsMockState = { isProd: false, isDev: false, isTest: true, isHosted: false, + isCopilotBillingAttributionV1Enabled: false, + isCopilotBillingProtocolRequired: false, isBillingEnabled: false, isEmailVerificationEnabled: false, isAuthDisabled: false, isPrivateDatabaseHostsAllowed: false, isRegistrationDisabled: false, - isEmailPasswordEnabled: false, + isEmailPasswordEnabled: true, + isSignupMxValidationEnabled: false, + isAppConfigEnabled: false, isTriggerDevEnabled: false, isSsoEnabled: false, isAccessControlEnabled: false, @@ -28,18 +69,95 @@ export const envFlagsMock = { isWhitelabelingEnabled: false, isAuditLogsEnabled: false, isDataRetentionEnabled: false, + isDataDrainsEnabled: false, + isForkingEnabled: false, isE2bEnabled: false, isE2BDocEnabled: false, isOllamaConfigured: false, isAzureConfigured: false, + isCohereConfigured: false, isInvitationsDisabled: false, isPublicApiDisabled: false, isGoogleAuthDisabled: false, isGithubAuthDisabled: false, + isMicrosoftAuthDisabled: false, + isEmailSignupDisabled: false, isReactGrabEnabled: false, isReactScanEnabled: false, - getAllowedIntegrationsFromEnv: vi.fn().mockReturnValue(null), - getBlacklistedProvidersFromEnv: vi.fn().mockReturnValue([]), - getAllowedMcpDomainsFromEnv: vi.fn().mockReturnValue(null), - getCostMultiplier: vi.fn().mockReturnValue(1), } + +const envFlagsState: EnvFlagsMockState = { ...defaultEnvFlagsState } + +/** + * Controllable mock functions for the function exports of + * `@/lib/core/config/env-flags`. Override per-test, e.g. + * `envFlagsMockFns.getCostMultiplier.mockReturnValue(2)`. + * {@link resetEnvFlagsMock} restores the default implementations. + */ +export const envFlagsMockFns = { + getAllowedIntegrationsFromEnv: vi.fn<() => string[] | null>(() => null), + getPreviewBlocksFromEnv: vi.fn<() => string[]>(() => []), + getBlacklistedProvidersFromEnv: vi.fn<() => string[]>(() => []), + getAllowedMcpDomainsFromEnv: vi.fn<() => string[] | null>(() => null), + getCostMultiplier: vi.fn<() => number>(() => 1), +} + +/** + * Applies per-test overrides to the shared env-flags mock state. + * Reads through the mocked module observe the new values immediately. + * + * @example + * ```ts + * beforeEach(() => { + * setEnvFlags({ isBillingEnabled: true, isHosted: true }) + * }) + * afterAll(resetEnvFlagsMock) + * ``` + */ +export function setEnvFlags(overrides: Partial): void { + Object.assign(envFlagsState, overrides) +} + +/** + * Restores the shared env-flags mock to its defaults: default flag state and + * default implementations for the function exports. + */ +export function resetEnvFlagsMock(): void { + Object.assign(envFlagsState, defaultEnvFlagsState) + envFlagsMockFns.getAllowedIntegrationsFromEnv.mockReset().mockImplementation(() => null) + envFlagsMockFns.getPreviewBlocksFromEnv.mockReset().mockImplementation(() => []) + envFlagsMockFns.getBlacklistedProvidersFromEnv.mockReset().mockImplementation(() => []) + envFlagsMockFns.getAllowedMcpDomainsFromEnv.mockReset().mockImplementation(() => null) + envFlagsMockFns.getCostMultiplier.mockReset().mockImplementation(() => 1) +} + +/** + * Builds a live get/set accessor pair for one flag so both reads through the + * mocked module and direct assignments (`envFlagsMock.isHosted = true`) + * delegate to the shared mutable state. + */ +function flagAccessor(key: keyof EnvFlagsMockState): PropertyDescriptor { + return { + enumerable: true, + get: () => envFlagsState[key], + set: (value: boolean) => { + envFlagsState[key] = value + }, + } +} + +/** + * Complete, stateful mock module for `@/lib/core/config/env-flags`, installed + * globally in `apps/sim/vitest.setup.ts`. Every export of the real module is + * present. Flag reads are live: override via {@link setEnvFlags} (or direct + * property assignment) and restore with {@link resetEnvFlagsMock}. + */ +export const envFlagsMock: EnvFlagsMockState & typeof envFlagsMockFns = Object.defineProperties( + { ...envFlagsMockFns } as EnvFlagsMockState & typeof envFlagsMockFns, + Object.fromEntries( + (Object.keys(defaultEnvFlagsState) as (keyof EnvFlagsMockState)[]).map((key) => [ + key, + flagAccessor(key), + ]) + ) +) diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index e12a09a6b48..e3f6c2d6f62 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -53,7 +53,13 @@ export { encryptionMock, encryptionMockFns } from './encryption.mock' // Env mocks export { createEnvMock, createMockGetEnv, defaultMockEnv, envMock } from './env.mock' // Env flag mocks -export { envFlagsMock } from './env-flags.mock' +export { + type EnvFlagsMockState, + envFlagsMock, + envFlagsMockFns, + resetEnvFlagsMock, + setEnvFlags, +} from './env-flags.mock' // Execution preprocessing mocks (for @/lib/execution/preprocessing) export { executionPreprocessingMock,