Skip to content

Commit d4542e4

Browse files
authored
fix(billing): coerce COST_MULTIPLIER to a number before sandbox pricing (#7399)
* fix(billing): coerce COST_MULTIPLIER to a number before sandbox pricing * fix(config): treat whitespace-only numeric env values as unset in envNumber
1 parent 7ee1a1f commit d4542e4

5 files changed

Lines changed: 60 additions & 6 deletions

File tree

apps/sim/lib/billing/sandbox-pricing.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
import { describe, expect, it } from 'vitest'
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { resetEnvMock, setEnv } from '@sim/testing/mocks/env.mock'
5+
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
6+
7+
vi.hoisted(() => {
8+
vi.stubEnv('NODE_ENV', 'production')
9+
})
10+
11+
vi.unmock('@/lib/core/config/env-flags')
12+
213
import { createSandboxPricing, priceSandboxUsage } from '@/lib/billing/sandbox-pricing'
314

15+
afterEach(resetEnvMock)
16+
afterAll(() => vi.unstubAllEnvs())
17+
418
describe('sandbox pricing', () => {
519
it.each([
620
['e2b', 0.1656],
@@ -33,4 +47,32 @@ describe('sandbox pricing', () => {
3347
'finite nonnegative'
3448
)
3549
})
50+
51+
describe('default multiplier from the production environment', () => {
52+
it('coerces the string COST_MULTIPLIER that process.env delivers', () => {
53+
setEnv({ COST_MULTIPLIER: '1.1' })
54+
55+
const pricing = createSandboxPricing('e2b')
56+
57+
expect(pricing.multiplier).toBe(1.1)
58+
expect(priceSandboxUsage(pricing, 1000, 1000).billedCost).toBeCloseTo(0.0000506, 8)
59+
})
60+
61+
it('falls back to 1 when COST_MULTIPLIER is unset', () => {
62+
setEnv({ COST_MULTIPLIER: undefined })
63+
64+
expect(createSandboxPricing('daytona').multiplier).toBe(1)
65+
})
66+
67+
it('falls back to 1 instead of throwing when COST_MULTIPLIER is not a nonnegative number', () => {
68+
setEnv({ COST_MULTIPLIER: 'abc' })
69+
expect(createSandboxPricing('e2b').multiplier).toBe(1)
70+
71+
setEnv({ COST_MULTIPLIER: '-2' })
72+
expect(createSandboxPricing('e2b').multiplier).toBe(1)
73+
74+
setEnv({ COST_MULTIPLIER: ' ' })
75+
expect(createSandboxPricing('e2b').multiplier).toBe(1)
76+
})
77+
})
3678
})

apps/sim/lib/core/config/env-flags.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
resolveEnterpriseEntitlement,
1515
resolveSandboxFeatureAvailability,
1616
} from './enterprise-entitlements'
17-
import { env, envBoolean, getEnv, isFalsy, isTruthy } from './env'
17+
import { env, envBoolean, envNumber, getEnv, isFalsy, isTruthy } from './env'
1818
import { hasEnvCapabilityValue, inspectCapability, SANDBOX_CAPABILITY } from './env-capabilities'
1919

2020
/**
@@ -684,8 +684,13 @@ export function getAllowedMcpDomainsFromEnv(): string[] | null {
684684
}
685685

686686
/**
687-
* Get cost multiplier based on environment
687+
* Get cost multiplier based on environment.
688+
*
689+
* `COST_MULTIPLIER` is declared as a number but arrives as a string from
690+
* `process.env` because `createEnv` skips validation, so it is normalized
691+
* through {@link envNumber}. Unset, empty, non-numeric, and negative values
692+
* fall back to 1.
688693
*/
689694
export function getCostMultiplier(): number {
690-
return isProd ? (env.COST_MULTIPLIER ?? 1) : 1
695+
return isProd ? envNumber(env.COST_MULTIPLIER, 1) : 1
691696
}

apps/sim/lib/core/config/env.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@ describe('envNumber', () => {
1212
expect(envNumber('5.5', 1, { min: 1, integer: true })).toBe(1)
1313
expect(envNumber(5.5, 1, { min: 1, integer: true })).toBe(1)
1414
})
15+
16+
it('treats whitespace-only values as unset instead of coercing them to 0', () => {
17+
expect(envNumber(' ', 1)).toBe(1)
18+
expect(envNumber('', 1)).toBe(1)
19+
expect(envNumber(' 1.1 ', 1)).toBe(1.1)
20+
expect(envNumber('0', 1)).toBe(0)
21+
})
1522
})

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ export function envNumber(
813813
) {
814814
return value
815815
}
816-
if (value === undefined || value === null || value === '') return fallback
816+
if (value === undefined || value === null || String(value).trim() === '') return fallback
817817
const parsed = Number(value)
818818
return Number.isFinite(parsed) && parsed >= min && (!options.integer || Number.isInteger(parsed))
819819
? parsed

packages/testing/src/mocks/env.mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export function envNumberImpl(
136136
) {
137137
return value
138138
}
139-
if (value === undefined || value === null || value === '') return fallback
139+
if (value === undefined || value === null || String(value).trim() === '') return fallback
140140
const parsed = Number(value)
141141
return Number.isFinite(parsed) && parsed >= min && (!options.integer || Number.isInteger(parsed))
142142
? parsed

0 commit comments

Comments
 (0)