Files
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

212 lines
7.2 KiB
TypeScript

import * as fs from 'fs';
import dotenv from 'dotenv';
import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest';
import logger from '../../src/logger';
import * as configManage from '../../src/util/config/manage';
import {
getConfigDirectoryPath,
refreshConfigDirectoryPathFromEnv,
setConfigDirectoryPath,
} from '../../src/util/config/manage';
import { setupEnv } from '../../src/util/env';
import { mockProcessEnv } from './utils';
vi.mock('fs', async () => {
const actual = await vi.importActual<typeof import('fs')>('fs');
return {
...actual,
existsSync: vi.fn(),
};
});
describe('setupEnv', () => {
let originalEnv: typeof process.env;
let dotenvConfigSpy: MockInstance<
(options?: dotenv.DotenvConfigOptions) => dotenv.DotenvConfigOutput
>;
let loggerInfoSpy: MockInstance;
beforeEach(() => {
originalEnv = { ...process.env };
// Ensure NODE_ENV is not set at the start of each test
mockProcessEnv({ NODE_ENV: undefined });
mockProcessEnv({ PROMPTFOO_CONFIG_DIR: undefined });
refreshConfigDirectoryPathFromEnv();
setConfigDirectoryPath(undefined);
// Spy on dotenv.config to verify it's called with the right parameters
dotenvConfigSpy = vi.spyOn(dotenv, 'config').mockImplementation(() => ({ parsed: {} }));
loggerInfoSpy = vi.spyOn(logger, 'info').mockImplementation(() => logger);
// Mock file existence check - default to true for backward compat with existing tests
vi.mocked(fs.existsSync).mockReturnValue(true);
});
afterEach(() => {
mockProcessEnv(originalEnv, { clear: true });
refreshConfigDirectoryPathFromEnv();
setConfigDirectoryPath(undefined);
vi.resetAllMocks();
});
it('should call dotenv.config with quiet=true when envPath is undefined', () => {
setupEnv(undefined);
expect(dotenvConfigSpy).toHaveBeenCalledTimes(1);
expect(dotenvConfigSpy).toHaveBeenCalledWith({ quiet: true });
});
it('should call dotenv.config with path, override=true, and quiet=true when envPath is specified', () => {
const testEnvPath = '.env.test';
setupEnv(testEnvPath);
expect(dotenvConfigSpy).toHaveBeenCalledTimes(1);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: testEnvPath,
override: true,
quiet: true,
});
expect(loggerInfoSpy).toHaveBeenCalledWith('Loading environment variables from .env.test');
});
it('should load environment variables with override when specified env file has conflicting values', () => {
// Mock dotenv.config to simulate loading variables
dotenvConfigSpy.mockImplementation((options?: dotenv.DotenvConfigOptions) => {
if (options?.path === '.env.production') {
if (options.override) {
mockProcessEnv({ NODE_ENV: 'production' });
} else if (!process.env.NODE_ENV) {
mockProcessEnv({ NODE_ENV: 'production' });
}
} else {
// Default .env file
if (!process.env.NODE_ENV) {
mockProcessEnv({ NODE_ENV: 'development' });
}
}
return { parsed: {} };
});
// First load the default .env (setting NODE_ENV to 'development')
setupEnv(undefined);
expect(process.env.NODE_ENV).toBe('development');
// Then load .env.production with override (should change NODE_ENV to 'production')
setupEnv('.env.production');
expect(process.env.NODE_ENV).toBe('production');
});
it('should refresh the config directory after early env loading and freeze later changes', () => {
const refreshConfigDirectorySpy = vi.spyOn(configManage, 'refreshConfigDirectoryPathFromEnv');
dotenvConfigSpy.mockImplementation(() => {
mockProcessEnv({ PROMPTFOO_CONFIG_DIR: '/early/config' });
return { parsed: {} };
});
setupEnv('.env.early', { refreshConfigDirectory: true });
expect(refreshConfigDirectorySpy).toHaveBeenCalledTimes(1);
expect(getConfigDirectoryPath()).toBe('/early/config');
mockProcessEnv({ PROMPTFOO_CONFIG_DIR: '/late/config' });
expect(getConfigDirectoryPath()).toBe('/early/config');
});
describe('multi-file support', () => {
it('should call dotenv.config with array of paths when multiple files specified', () => {
const paths = ['.env', '.env.local'];
setupEnv(paths);
expect(dotenvConfigSpy).toHaveBeenCalledTimes(1);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: paths,
override: true,
quiet: true,
});
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Loading environment variables from: .env, .env.local',
);
});
it('should call dotenv.config with single path (not array) when one file specified as array', () => {
const paths = ['.env'];
setupEnv(paths);
expect(dotenvConfigSpy).toHaveBeenCalledTimes(1);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: '.env',
override: true,
quiet: true,
});
expect(loggerInfoSpy).toHaveBeenCalledWith('Loading environment variables from .env');
});
it('should throw error when specified file does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
expect(() => setupEnv('.env.missing')).toThrow('Environment file not found: .env.missing');
});
it('should throw error when any file in array does not exist', () => {
vi.mocked(fs.existsSync).mockImplementation((p) => p === '.env');
expect(() => setupEnv(['.env', '.env.missing'])).toThrow(
'Environment file not found: .env.missing',
);
});
it('should validate all files exist before calling dotenv.config', () => {
vi.mocked(fs.existsSync).mockImplementation((p) => p === '.env');
expect(() => setupEnv(['.env', '.env.missing'])).toThrow();
expect(dotenvConfigSpy).not.toHaveBeenCalled();
});
it('should filter out empty strings from array', () => {
setupEnv(['', '.env', ' ']);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: '.env',
override: true,
quiet: true,
});
expect(loggerInfoSpy).toHaveBeenCalledWith('Loading environment variables from .env');
});
it('should call default dotenv.config when array contains only empty strings', () => {
setupEnv(['', ' ', '']);
expect(dotenvConfigSpy).toHaveBeenCalledWith({ quiet: true });
});
it('should call default dotenv.config when given empty array', () => {
setupEnv([]);
expect(dotenvConfigSpy).toHaveBeenCalledWith({ quiet: true });
});
it('should expand comma-separated values within array elements', () => {
// This simulates what Commander passes when using --env-file .env,.env.local
setupEnv(['.env,.env.local']);
expect(dotenvConfigSpy).toHaveBeenCalledTimes(1);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: ['.env', '.env.local'],
override: true,
quiet: true,
});
});
it('should handle mixed array with some comma-separated and some individual paths', () => {
setupEnv(['.env,.env.local', '.env.production']);
expect(dotenvConfigSpy).toHaveBeenCalledWith({
path: ['.env', '.env.local', '.env.production'],
override: true,
quiet: true,
});
});
});
});