Files
promptfoo--promptfoo/test/envars.test.ts
T
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

480 lines
17 KiB
TypeScript

import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import cliState from '../src/cliState';
import {
getEnvBool,
getEnvFloat,
getEnvInt,
getEnvString,
getMaxEvalTimeMs,
isCI,
} from '../src/envars';
import { setEnvOverridesProvider } from '../src/envOverrides';
import { mockProcessEnv } from './util/utils';
import type { EnvVarKey } from '../src/envars';
describe('envars', () => {
const originalEnv = { ...process.env };
const originalCliState = { ...cliState };
beforeEach(() => {
vi.resetModules();
mockProcessEnv({ ...originalEnv }, { clear: true });
// Reset cliState to empty for each test
Object.keys(cliState).forEach((key) => {
delete cliState[key as keyof typeof cliState];
});
setEnvOverridesProvider(() => cliState.config?.env);
});
afterEach(() => {
// Clear the throwing cliState mock used by the "without importing cliState" test
// before the next test's beforeEach re-resolves modules.
vi.doUnmock('../src/cliState');
});
afterAll(() => {
mockProcessEnv(originalEnv, { clear: true });
// Restore original cliState
Object.keys(cliState).forEach((key) => {
delete cliState[key as keyof typeof cliState];
});
Object.assign(cliState, originalCliState);
// Symmetric teardown: leave the singleton "unregistered" rather than pointing
// at a closure owned by this test file.
setEnvOverridesProvider(undefined);
});
describe('getEnvar', () => {
it('should return the value of an existing environment variable', () => {
mockProcessEnv({ PROMPTFOO_AUTHOR: 'test value' });
expect(getEnvString('PROMPTFOO_AUTHOR')).toBe('test value');
});
it('should return undefined for a non-existing environment variable', () => {
expect(getEnvString('PROMPTFOO_AUTHOR')).toBeUndefined();
});
it('should return the default value for a non-existing environment variable', () => {
expect(getEnvString('PROMPTFOO_AUTHOR', 'default')).toBe('default');
});
it('should prioritize cliState.config.env over process.env', () => {
mockProcessEnv({ OPENAI_API_KEY: 'process-env-key' });
cliState.config = {
env: {
OPENAI_API_KEY: 'config-env-key',
},
};
expect(getEnvString('OPENAI_API_KEY')).toBe('config-env-key');
});
it('should convert non-string values from cliState.config.env to strings', () => {
cliState.config = {
env: {
OPENAI_TEMPERATURE: 0.7 as any,
PROMPTFOO_CACHE_ENABLED: true as any,
},
};
expect(getEnvString('OPENAI_TEMPERATURE')).toBe('0.7');
expect(getEnvString('PROMPTFOO_CACHE_ENABLED')).toBe('true');
});
it('should handle HTTP proxy environment variables', () => {
mockProcessEnv({ HTTP_PROXY: 'http://proxy.example.com:8080' });
mockProcessEnv({ HTTPS_PROXY: 'https://proxy.example.com:8443' });
expect(getEnvString('HTTP_PROXY')).toBe('http://proxy.example.com:8080');
expect(getEnvString('HTTPS_PROXY')).toBe('https://proxy.example.com:8443');
});
it('should handle provider-specific environment variables', () => {
mockProcessEnv({ CDP_DOMAIN: 'custom.domain' });
mockProcessEnv({ PORTKEY_API_BASE_URL: 'https://api.portkey.example.com' });
expect(getEnvString('CDP_DOMAIN')).toBe('custom.domain');
expect(getEnvString('PORTKEY_API_BASE_URL')).toBe('https://api.portkey.example.com');
});
it('should handle arbitrary string keys not defined in EnvVars type', () => {
mockProcessEnv({ CUSTOM_ENV_VAR: 'custom value' });
expect(getEnvString('CUSTOM_ENV_VAR' as EnvVarKey)).toBe('custom value');
});
it('should read injected env overrides without importing cliState', async () => {
vi.resetModules();
vi.doMock('../src/cliState', () => {
throw new Error('cliState should not be imported by envars');
});
const [{ getEnvString }, { setEnvOverridesProvider }] = await Promise.all([
import('../src/envars'),
import('../src/envOverrides'),
]);
setEnvOverridesProvider(() => ({ OPENAI_API_KEY: 'provider-env-key' }));
expect(getEnvString('OPENAI_API_KEY')).toBe('provider-env-key');
});
it('should fall through to process.env when no provider is registered', () => {
mockProcessEnv({ OPENAI_API_KEY: 'process-env-key' });
setEnvOverridesProvider(undefined);
expect(getEnvString('OPENAI_API_KEY')).toBe('process-env-key');
});
it('should fall through to process.env when the provider returns undefined', () => {
mockProcessEnv({ OPENAI_API_KEY: 'process-env-key' });
setEnvOverridesProvider(() => undefined);
expect(getEnvString('OPENAI_API_KEY')).toBe('process-env-key');
});
it('should fall through to process.env when the provider returns an empty record', () => {
mockProcessEnv({ OPENAI_API_KEY: 'process-env-key' });
setEnvOverridesProvider(() => ({}));
expect(getEnvString('OPENAI_API_KEY')).toBe('process-env-key');
});
it('should swallow provider exceptions and fall through to process.env', () => {
mockProcessEnv({ OPENAI_API_KEY: 'process-env-key' });
setEnvOverridesProvider(() => {
throw new Error('provider exploded');
});
expect(() => getEnvString('OPENAI_API_KEY')).not.toThrow();
expect(getEnvString('OPENAI_API_KEY')).toBe('process-env-key');
});
it('should auto-register the provider when cliState is imported', async () => {
vi.resetModules();
const [dynEnvOverrides, dynCliState] = await Promise.all([
import('../src/envOverrides'),
import('../src/cliState'),
]);
dynCliState.default.config = { env: { OPENAI_API_KEY: 'wired-key' } };
expect(dynEnvOverrides.getEnvOverrides()).toEqual({ OPENAI_API_KEY: 'wired-key' });
});
});
describe('getEnvBool', () => {
it('should return true for truthy string values', () => {
['1', 'true', 'yes', 'yup'].forEach((value) => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: value });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(true);
});
});
it('should explicitly treat "yeppers" as a truthy value', () => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: 'yeppers' });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(true);
});
it('should return false for falsy string values', () => {
['0', 'false', 'no', 'nope'].forEach((value) => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: value });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(false);
});
});
it('should return the default value for a non-existing environment variable', () => {
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED', true)).toBe(true);
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED', false)).toBe(false);
});
it('should return false for any other string values', () => {
['maybe', 'enabled', 'on'].forEach((value) => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: value });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(false);
});
});
it('should return true when the environment variable is set to "1"', () => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: '1' });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(true);
});
it('should return false when the environment variable is set to "0"', () => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: '0' });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(false);
});
it('should return false when no default value is provided and the environment variable is not set', () => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: undefined });
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(false);
});
it('should prioritize cliState.config.env over process.env for boolean values', () => {
mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: 'false' });
cliState.config = {
env: {
PROMPTFOO_CACHE_ENABLED: true as any,
},
};
expect(getEnvBool('PROMPTFOO_CACHE_ENABLED')).toBe(true);
});
it('should handle arbitrary string keys for boolean values', () => {
mockProcessEnv({ CUSTOM_BOOL_VAR: 'true' });
expect(getEnvBool('CUSTOM_BOOL_VAR' as EnvVarKey)).toBe(true);
});
it('should handle PROMPTFOO_DISABLE_OBJECT_STRINGIFY environment variable', () => {
expect(getEnvBool('PROMPTFOO_DISABLE_OBJECT_STRINGIFY')).toBe(false);
mockProcessEnv({ PROMPTFOO_DISABLE_OBJECT_STRINGIFY: 'true' });
expect(getEnvBool('PROMPTFOO_DISABLE_OBJECT_STRINGIFY')).toBe(true);
mockProcessEnv({ PROMPTFOO_DISABLE_OBJECT_STRINGIFY: 'false' });
expect(getEnvBool('PROMPTFOO_DISABLE_OBJECT_STRINGIFY')).toBe(false);
cliState.config = {
env: {
PROMPTFOO_DISABLE_OBJECT_STRINGIFY: true as any,
},
};
expect(getEnvBool('PROMPTFOO_DISABLE_OBJECT_STRINGIFY')).toBe(true);
});
});
describe('getEnvInt', () => {
it('should return the integer value of an existing environment variable', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '42' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBe(42);
});
it('should return undefined for a non-numeric environment variable', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: 'not a number' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBeUndefined();
});
it('should return the default value for a non-existing environment variable', () => {
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT', 100)).toBe(100);
});
it('should floor a floating-point number in the environment variable', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '42.7' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBe(42);
});
it('should handle negative numbers', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '-42' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBe(-42);
});
it('should return undefined for empty string', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBeUndefined();
});
it('should return the default value when the environment variable is undefined', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: undefined });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT', 100)).toBe(100);
});
it('should return undefined when no default value is provided and the environment variable is not set', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: undefined });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBeUndefined();
});
it('should prioritize cliState.config.env over process.env for integer values', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '100' });
cliState.config = {
env: {
PROMPTFOO_CACHE_MAX_FILE_COUNT: 42 as any,
},
};
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBe(42);
});
it('should handle arbitrary string keys for integer values', () => {
mockProcessEnv({ CUSTOM_INT_VAR: '123' });
expect(getEnvInt('CUSTOM_INT_VAR' as EnvVarKey)).toBe(123);
});
it('should return 0 when environment variable is set to "0"', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '0' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT')).toBe(0);
});
it('should return 0 instead of default when environment variable is "0"', () => {
mockProcessEnv({ PROMPTFOO_CACHE_MAX_FILE_COUNT: '0' });
expect(getEnvInt('PROMPTFOO_CACHE_MAX_FILE_COUNT', 100)).toBe(0);
});
});
describe('getEnvFloat', () => {
it('should return the float value of an existing environment variable', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '3.14' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBe(3.14);
});
it('should return undefined for a non-numeric environment variable', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: 'not a number' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBeUndefined();
});
it('should return the default value for a non-existing environment variable', () => {
expect(getEnvFloat('OPENAI_TEMPERATURE', 2.718)).toBe(2.718);
});
it('should handle integer values', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '42' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBe(42);
});
it('should handle negative numbers', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '-3.14' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBe(-3.14);
});
it('should return undefined for empty string', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBeUndefined();
});
it('should return the default value when the environment variable is undefined', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: undefined });
expect(getEnvFloat('OPENAI_TEMPERATURE', 2.718)).toBe(2.718);
});
it('should return undefined when no default value is provided and the environment variable is not set', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: undefined });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBeUndefined();
});
it('should return 0 when environment variable is set to "0"', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '0' });
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBe(0);
});
it('should return 0 instead of default when environment variable is "0"', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '0' });
expect(getEnvFloat('OPENAI_TEMPERATURE', 0.7)).toBe(0);
});
it('should prioritize cliState.config.env over process.env for float values', () => {
mockProcessEnv({ OPENAI_TEMPERATURE: '1.0' });
cliState.config = {
env: {
OPENAI_TEMPERATURE: 0.7 as any,
},
};
expect(getEnvFloat('OPENAI_TEMPERATURE')).toBe(0.7);
});
it('should handle arbitrary string keys for float values', () => {
mockProcessEnv({ CUSTOM_FLOAT_VAR: '3.14159' });
expect(getEnvFloat('CUSTOM_FLOAT_VAR' as EnvVarKey)).toBe(3.14159);
});
});
describe('isCI', () => {
const ciEnvironments = [
'CI',
'GITHUB_ACTIONS',
'TRAVIS',
'CIRCLECI',
'JENKINS',
'GITLAB_CI',
'APPVEYOR',
'CODEBUILD_BUILD_ID',
'TF_BUILD',
'BITBUCKET_COMMIT',
'BUDDY',
'BUILDKITE',
'TEAMCITY_VERSION',
];
beforeEach(() => {
// Clear all CI-related environment variables before each test
ciEnvironments.forEach((env) => mockProcessEnv({ [env]: undefined }));
});
it('should return false when no CI environment variables are set', () => {
expect(isCI()).toBe(false);
});
ciEnvironments.forEach((env) => {
it(`should return true when ${env} is set to 'true'`, () => {
mockProcessEnv({ [env]: 'true' });
expect(isCI()).toBe(true);
});
it(`should return false when ${env} is set to 'false'`, () => {
mockProcessEnv({ [env]: 'false' });
expect(isCI()).toBe(false);
});
});
it('should return true if any CI environment variable is set to true', () => {
mockProcessEnv({ GITHUB_ACTIONS: 'true' });
mockProcessEnv({ TRAVIS: 'false' });
expect(isCI()).toBe(true);
});
it('should prioritize cliState.config.env over process.env for CI detection', () => {
mockProcessEnv({ CI: 'false' });
cliState.config = {
env: {
CI: 'true',
},
};
expect(isCI()).toBe(true);
});
});
describe('getMaxEvalTimeMs', () => {
it('should return default value when environment variable is not set', () => {
expect(getMaxEvalTimeMs()).toBe(0);
expect(getMaxEvalTimeMs(5000)).toBe(5000);
});
it('should return parsed integer value from environment variable', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: '10000' });
expect(getMaxEvalTimeMs()).toBe(10000);
});
it('should handle invalid values', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: 'invalid' });
expect(getMaxEvalTimeMs(5000)).toBe(5000);
});
it('should prioritize cliState.config.env over process.env', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: '5000' });
cliState.config = {
env: {
PROMPTFOO_MAX_EVAL_TIME_MS: 10000 as any,
},
};
expect(getMaxEvalTimeMs()).toBe(10000);
});
it('should floor floating point values', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: '1234.56' });
expect(getMaxEvalTimeMs()).toBe(1234);
});
it('should handle negative values', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: '-1000' });
expect(getMaxEvalTimeMs()).toBe(-1000);
});
it('should handle empty string', () => {
mockProcessEnv({ PROMPTFOO_MAX_EVAL_TIME_MS: '' });
expect(getMaxEvalTimeMs(1000)).toBe(1000);
});
});
});