Files
wehub-resource-sync 0d3cb498a3
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
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

245 lines
7.9 KiB
TypeScript

import * as os from 'os';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import cliState from '../../../src/cliState';
import logger from '../../../src/logger';
import {
getConfigDirectoryPath,
refreshConfigDirectoryPathFromEnv,
setConfigDirectoryPath,
} from '../../../src/util/config/manage';
import { writePromptfooConfig } from '../../../src/util/config/writer';
import { mockProcessEnv } from '../../util/utils';
import type { UnifiedConfig } from '../../../src/types/index';
// Create hoisted mock functions for fs
const mockFs = vi.hoisted(() => ({
existsSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
readFileSync: vi.fn(),
readdirSync: vi.fn(),
statSync: vi.fn(),
unlinkSync: vi.fn(),
rmdirSync: vi.fn(),
copyFileSync: vi.fn(),
renameSync: vi.fn(),
accessSync: vi.fn(),
chmodSync: vi.fn(),
constants: {},
}));
// Mock both 'fs' and 'node:fs' to cover all import patterns
vi.mock('fs', () => ({
...mockFs,
default: mockFs,
}));
vi.mock('node:fs', () => ({
...mockFs,
default: mockFs,
}));
vi.mock('os');
// js-yaml v5 is native ESM with a sealed namespace, so vi.spyOn cannot patch it.
// Wrap dump in a spy-able mock that keeps the real implementation.
vi.mock('js-yaml', async (importOriginal) => {
const actual = await importOriginal<typeof import('js-yaml')>();
return { ...actual, dump: vi.fn(actual.dump) };
});
vi.mock('../../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
describe('config', () => {
const mockHomedir = '/mock/home';
const defaultConfigPath = path.join(mockHomedir, '.promptfoo');
let restoreEnv: () => void;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue(mockHomedir);
mockFs.existsSync.mockReturnValue(false);
restoreEnv = mockProcessEnv({ PROMPTFOO_CONFIG_DIR: undefined });
refreshConfigDirectoryPathFromEnv();
setConfigDirectoryPath(undefined);
});
afterEach(() => {
restoreEnv();
refreshConfigDirectoryPathFromEnv();
setConfigDirectoryPath(undefined);
});
describe('getConfigDirectoryPath', () => {
it('returns default path when no custom path is set', () => {
expect(getConfigDirectoryPath()).toBe(defaultConfigPath);
});
it('reads a config directory refreshed after early env-file loading', () => {
const restoreConfigDir = mockProcessEnv({ PROMPTFOO_CONFIG_DIR: '/env-file/config' });
try {
refreshConfigDirectoryPathFromEnv();
expect(getConfigDirectoryPath()).toBe('/env-file/config');
} finally {
restoreConfigDir();
refreshConfigDirectoryPathFromEnv();
}
});
it('does not let eval config env overrides move the global config directory', () => {
const originalConfig = cliState.config;
cliState.config = { env: { PROMPTFOO_CONFIG_DIR: '/eval-config/path' } };
try {
expect(getConfigDirectoryPath()).toBe(defaultConfigPath);
} finally {
cliState.config = originalConfig;
}
});
it('does not move after later process environment changes', () => {
const restoreConfigDir = mockProcessEnv({ PROMPTFOO_CONFIG_DIR: '/late/config' });
try {
expect(getConfigDirectoryPath()).toBe(defaultConfigPath);
} finally {
restoreConfigDir();
}
});
it('does not create directory when createIfNotExists is false', () => {
getConfigDirectoryPath(false);
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
});
// Note: The directory creation test cannot verify mkdirSync was called because
// the source uses require('fs') inside the function which bypasses Vitest's ESM mocking.
// We verify the function completes successfully with the correct return value instead.
it('handles createIfNotExists flag and returns correct path', () => {
const result = getConfigDirectoryPath(true);
expect(result).toBe(defaultConfigPath);
});
it('does not create directory when it already exists', () => {
mockFs.existsSync.mockReturnValue(true);
const result = getConfigDirectoryPath(true);
expect(result).toBe(defaultConfigPath);
});
});
describe('setConfigDirectoryPath', () => {
it('updates the config directory path', () => {
const newPath = '/new/config/path';
setConfigDirectoryPath(newPath);
expect(getConfigDirectoryPath()).toBe(newPath);
});
it('overrides the environment variable', () => {
const envPath = '/env/path';
const newPath = '/new/path';
const restoreConfigDir = mockProcessEnv({ PROMPTFOO_CONFIG_DIR: envPath });
try {
setConfigDirectoryPath(newPath);
expect(getConfigDirectoryPath()).toBe(newPath);
} finally {
restoreConfigDir();
}
});
});
});
describe('writePromptfooConfig', () => {
const mockOutputPath = '/mock/output/path.yaml';
beforeEach(() => {
vi.resetAllMocks();
});
it('writes a basic config to the specified path', () => {
const mockConfig: Partial<UnifiedConfig> = { description: 'Test config' };
const mockYaml = 'description: Test config\n';
vi.mocked(yaml.dump).mockReturnValue(mockYaml);
writePromptfooConfig(mockConfig, mockOutputPath);
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
mockOutputPath,
`# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json\n${mockYaml}`,
);
});
it('orders the keys of the config correctly', () => {
const mockConfig: Partial<UnifiedConfig> = {
tests: [{ assert: [{ type: 'equals', value: 'test assertion' }] }],
description: 'Test config',
prompts: ['prompt1'],
providers: ['provider1'],
defaultTest: { assert: [{ type: 'equals', value: 'default assertion' }] },
};
writePromptfooConfig(mockConfig, mockOutputPath);
const dumpCall = vi.mocked(yaml.dump).mock.calls[0][0];
const keys = Object.keys(dumpCall);
expect(keys).toEqual(['description', 'prompts', 'providers', 'defaultTest', 'tests']);
});
it('uses js-yaml to dump the config with skipInvalid option', () => {
const mockConfig: Partial<UnifiedConfig> = { description: 'Test config' };
writePromptfooConfig(mockConfig, mockOutputPath);
expect(yaml.dump).toHaveBeenCalledWith(expect.anything(), { skipInvalid: true });
});
it('handles empty config', () => {
vi.mocked(yaml.dump).mockReturnValueOnce('');
const mockConfig: Partial<UnifiedConfig> = {};
writePromptfooConfig(mockConfig, mockOutputPath);
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith('Warning: config is empty, skipping write');
});
it('preserves all fields of the UnifiedConfig', () => {
const mockConfig: Partial<UnifiedConfig> = {
description: 'Full config test',
prompts: ['prompt1', 'prompt2'],
providers: ['provider1', 'provider2'],
defaultTest: { assert: [{ type: 'equals', value: 'default assertion' }] },
tests: [
{ assert: [{ type: 'equals', value: 'test assertion 1' }] },
{ assert: [{ type: 'equals', value: 'test assertion 2' }] },
],
outputPath: './output',
};
writePromptfooConfig(mockConfig, mockOutputPath);
const dumpCall = vi.mocked(yaml.dump).mock.calls[0][0];
expect(dumpCall).toEqual(expect.objectContaining(mockConfig));
});
it('handles config with undefined values', () => {
const mockConfig: Partial<UnifiedConfig> = {
description: 'Config with undefined',
prompts: undefined,
providers: ['provider1'],
};
writePromptfooConfig(mockConfig, mockOutputPath);
const dumpCall = vi.mocked(yaml.dump).mock.calls[0][0];
expect(dumpCall).toHaveProperty('description', 'Config with undefined');
expect(dumpCall).toHaveProperty('providers', ['provider1']);
expect(dumpCall).not.toHaveProperty('prompts');
});
});