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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion apps/sim/lib/billing/sandbox-pricing.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { describe, expect, it } from 'vitest'
/**
* @vitest-environment node
*/
import { resetEnvMock, setEnv } from '@sim/testing/mocks/env.mock'
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
Comment thread
waleedlatif1 marked this conversation as resolved.

vi.hoisted(() => {
vi.stubEnv('NODE_ENV', 'production')
})

vi.unmock('@/lib/core/config/env-flags')

import { createSandboxPricing, priceSandboxUsage } from '@/lib/billing/sandbox-pricing'

afterEach(resetEnvMock)
afterAll(() => vi.unstubAllEnvs())

describe('sandbox pricing', () => {
it.each([
['e2b', 0.1656],
Expand Down Expand Up @@ -33,4 +47,32 @@ describe('sandbox pricing', () => {
'finite nonnegative'
)
})

describe('default multiplier from the production environment', () => {
it('coerces the string COST_MULTIPLIER that process.env delivers', () => {
setEnv({ COST_MULTIPLIER: '1.1' })

const pricing = createSandboxPricing('e2b')

expect(pricing.multiplier).toBe(1.1)
expect(priceSandboxUsage(pricing, 1000, 1000).billedCost).toBeCloseTo(0.0000506, 8)
})

it('falls back to 1 when COST_MULTIPLIER is unset', () => {
setEnv({ COST_MULTIPLIER: undefined })

expect(createSandboxPricing('daytona').multiplier).toBe(1)
})

it('falls back to 1 instead of throwing when COST_MULTIPLIER is not a nonnegative number', () => {
setEnv({ COST_MULTIPLIER: 'abc' })
expect(createSandboxPricing('e2b').multiplier).toBe(1)

setEnv({ COST_MULTIPLIER: '-2' })
expect(createSandboxPricing('e2b').multiplier).toBe(1)

setEnv({ COST_MULTIPLIER: ' ' })
expect(createSandboxPricing('e2b').multiplier).toBe(1)
})
})
})
11 changes: 8 additions & 3 deletions apps/sim/lib/core/config/env-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
resolveEnterpriseEntitlement,
resolveSandboxFeatureAvailability,
} from './enterprise-entitlements'
import { env, envBoolean, getEnv, isFalsy, isTruthy } from './env'
import { env, envBoolean, envNumber, getEnv, isFalsy, isTruthy } from './env'
import { hasEnvCapabilityValue, inspectCapability, SANDBOX_CAPABILITY } from './env-capabilities'

/**
Expand Down Expand Up @@ -684,8 +684,13 @@ export function getAllowedMcpDomainsFromEnv(): string[] | null {
}

/**
* Get cost multiplier based on environment
* Get cost multiplier based on environment.
*
* `COST_MULTIPLIER` is declared as a number but arrives as a string from
* `process.env` because `createEnv` skips validation, so it is normalized
* through {@link envNumber}. Unset, empty, non-numeric, and negative values
* fall back to 1.
*/
export function getCostMultiplier(): number {
return isProd ? (env.COST_MULTIPLIER ?? 1) : 1
return isProd ? envNumber(env.COST_MULTIPLIER, 1) : 1
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
}
7 changes: 7 additions & 0 deletions apps/sim/lib/core/config/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ describe('envNumber', () => {
expect(envNumber('5.5', 1, { min: 1, integer: true })).toBe(1)
expect(envNumber(5.5, 1, { min: 1, integer: true })).toBe(1)
})

it('treats whitespace-only values as unset instead of coercing them to 0', () => {
expect(envNumber(' ', 1)).toBe(1)
expect(envNumber('', 1)).toBe(1)
expect(envNumber(' 1.1 ', 1)).toBe(1.1)
expect(envNumber('0', 1)).toBe(0)
})
})
2 changes: 1 addition & 1 deletion apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ export function envNumber(
) {
return value
}
if (value === undefined || value === null || value === '') return fallback
if (value === undefined || value === null || String(value).trim() === '') return fallback
const parsed = Number(value)
return Number.isFinite(parsed) && parsed >= min && (!options.integer || Number.isInteger(parsed))
? parsed
Expand Down
2 changes: 1 addition & 1 deletion packages/testing/src/mocks/env.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function envNumberImpl(
) {
return value
}
if (value === undefined || value === null || value === '') return fallback
if (value === undefined || value === null || String(value).trim() === '') return fallback
const parsed = Number(value)
return Number.isFinite(parsed) && parsed >= min && (!options.integer || Number.isInteger(parsed))
? parsed
Expand Down
Loading