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
174 lines
5.3 KiB
TypeScript
174 lines
5.3 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 {
|
|
getConfigDirectoryPath,
|
|
refreshConfigDirectoryPathFromEnv,
|
|
setConfigDirectoryPath,
|
|
} from '../../../src/util/config/manage';
|
|
import { writePromptfooConfig } from '../../../src/util/config/writer';
|
|
import { mockProcessEnv } from '../../util/utils';
|
|
|
|
// 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,
|
|
}));
|
|
|
|
// 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) };
|
|
});
|
|
|
|
// Mock os module
|
|
vi.mock('os');
|
|
vi.mock('../../../src/logger', () => ({
|
|
default: {
|
|
warn: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('config management', () => {
|
|
let restoreEnv: () => void;
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
restoreEnv = mockProcessEnv({ PROMPTFOO_CONFIG_DIR: undefined });
|
|
refreshConfigDirectoryPathFromEnv();
|
|
setConfigDirectoryPath(undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreEnv();
|
|
refreshConfigDirectoryPathFromEnv();
|
|
setConfigDirectoryPath(undefined);
|
|
});
|
|
|
|
describe('getConfigDirectoryPath', () => {
|
|
it('should return default config path when no custom path set', () => {
|
|
setConfigDirectoryPath(undefined);
|
|
const configPath = getConfigDirectoryPath();
|
|
expect(configPath).toBe(path.join('/home/user', '.promptfoo'));
|
|
});
|
|
|
|
// 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('should handle createIfNotExists flag and return correct path', () => {
|
|
mockFs.existsSync.mockReturnValue(false);
|
|
setConfigDirectoryPath(undefined);
|
|
const result = getConfigDirectoryPath(true);
|
|
expect(result).toBe(path.join('/home/user', '.promptfoo'));
|
|
});
|
|
|
|
it('should not create directory if it already exists', () => {
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
const result = getConfigDirectoryPath(true);
|
|
expect(result).toBe(path.join('/home/user', '.promptfoo'));
|
|
});
|
|
});
|
|
|
|
describe('setConfigDirectoryPath', () => {
|
|
it('should set custom config directory path', () => {
|
|
const customPath = '/custom/path';
|
|
setConfigDirectoryPath(customPath);
|
|
const configPath = getConfigDirectoryPath();
|
|
expect(configPath).toBe(customPath);
|
|
});
|
|
|
|
it('should handle undefined path', () => {
|
|
setConfigDirectoryPath(undefined);
|
|
const configPath = getConfigDirectoryPath();
|
|
expect(configPath).toBe(path.join('/home/user', '.promptfoo'));
|
|
});
|
|
});
|
|
|
|
describe('writePromptfooConfig', () => {
|
|
const outputPath = 'config.yaml';
|
|
|
|
it('should write config with schema comment', () => {
|
|
const config = {
|
|
description: 'test config',
|
|
prompts: ['prompt1'],
|
|
};
|
|
|
|
writePromptfooConfig(config, outputPath);
|
|
|
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
outputPath,
|
|
`# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json\n${yaml.dump(config)}`,
|
|
);
|
|
});
|
|
|
|
it('should write config with header comments', () => {
|
|
const config = {
|
|
description: 'test config',
|
|
};
|
|
const headerComments = ['Comment 1', 'Comment 2'];
|
|
|
|
writePromptfooConfig(config, outputPath, headerComments);
|
|
|
|
const expectedContent =
|
|
`# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json\n` +
|
|
`# Comment 1\n# Comment 2\n${yaml.dump(config)}`;
|
|
|
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(outputPath, expectedContent);
|
|
});
|
|
|
|
it('should handle empty config', () => {
|
|
const config = {};
|
|
const result = writePromptfooConfig(config, outputPath);
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('should order config keys', () => {
|
|
const config = {
|
|
tests: ['test1'],
|
|
description: 'desc',
|
|
prompts: ['prompt1'],
|
|
};
|
|
|
|
const result = writePromptfooConfig(config, outputPath);
|
|
|
|
expect(Object.keys(result)[0]).toBe('description');
|
|
expect(Object.keys(result)[1]).toBe('prompts');
|
|
expect(Object.keys(result)[2]).toBe('tests');
|
|
});
|
|
|
|
it('should handle empty yaml content', () => {
|
|
vi.mocked(yaml.dump).mockReturnValueOnce('');
|
|
const config = { description: 'test' };
|
|
const result = writePromptfooConfig(config, outputPath);
|
|
expect(result).toEqual(config);
|
|
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|