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
336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import * as esmModule from '../../src/esm';
|
|
import logger from '../../src/logger';
|
|
import * as pythonUtils from '../../src/python/pythonUtils';
|
|
import { isJavascriptFile } from '../../src/util/fileExtensions';
|
|
import { loadFileReference, processConfigFileReferences } from '../../src/util/fileReference';
|
|
import { loadYaml } from '../../src/util/yamlLoad';
|
|
import type { Logger } from 'winston';
|
|
|
|
vi.mock('fs', async () => {
|
|
const actual = await vi.importActual<typeof import('fs')>('fs');
|
|
return {
|
|
...actual,
|
|
default: {
|
|
...actual,
|
|
promises: {
|
|
readFile: vi.fn(),
|
|
},
|
|
},
|
|
promises: {
|
|
readFile: vi.fn(),
|
|
},
|
|
};
|
|
});
|
|
vi.mock('path');
|
|
vi.mock('../../src/util/yamlLoad');
|
|
vi.mock('../../src/esm', () => ({
|
|
importModule: vi.fn(),
|
|
}));
|
|
vi.mock('../../src/python/pythonUtils', () => ({
|
|
runPython: vi.fn(),
|
|
}));
|
|
vi.mock('../../src/util/fileExtensions');
|
|
vi.mock('../../src/logger', () => ({
|
|
default: {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const importModule = vi.mocked(esmModule.importModule);
|
|
const runPython = vi.mocked(pythonUtils.runPython);
|
|
const readFileMock = vi.mocked(fs.promises.readFile);
|
|
|
|
describe('fileReference utility functions', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
|
|
vi.mocked(logger.debug).mockImplementation((_message: string) => {
|
|
return {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
} as unknown as Logger;
|
|
});
|
|
|
|
vi.mocked(logger.error).mockImplementation((_message: string) => {
|
|
return {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
} as unknown as Logger;
|
|
});
|
|
|
|
vi.mocked(path.resolve).mockImplementation((basePath, filePath) =>
|
|
filePath?.startsWith('/') ? filePath : path.join(basePath || '', filePath || ''),
|
|
);
|
|
|
|
vi.mocked(path.join).mockImplementation((...parts) => parts.filter(Boolean).join('/'));
|
|
|
|
vi.mocked(path.extname).mockImplementation((filePath) => {
|
|
if (!filePath) {
|
|
return '';
|
|
}
|
|
const parts = filePath.split('.');
|
|
return parts.length > 1 ? `.${parts[parts.length - 1]}` : '';
|
|
});
|
|
|
|
vi.mocked(isJavascriptFile).mockImplementation((filePath) => {
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
return ['.js', '.mjs', '.ts', '.cjs'].some((ext) => filePath.endsWith(ext));
|
|
});
|
|
|
|
importModule.mockResolvedValue({});
|
|
runPython.mockResolvedValue({});
|
|
});
|
|
|
|
describe('loadFileReference', () => {
|
|
it('should load JSON files correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.json';
|
|
const fileContent = '{"name": "test", "value": 42}';
|
|
const parsedContent = { name: 'test', value: 42 };
|
|
|
|
readFileMock.mockResolvedValue(fileContent);
|
|
vi.spyOn(JSON, 'parse').mockReturnValue(parsedContent);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/config.json', 'utf8');
|
|
expect(result).toEqual(parsedContent);
|
|
});
|
|
|
|
it('should load YAML files correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.yaml';
|
|
const fileContent = 'name: test\nvalue: 42';
|
|
const parsedContent = { name: 'test', value: 42 };
|
|
|
|
readFileMock.mockResolvedValue(fileContent);
|
|
vi.mocked(loadYaml).mockReturnValue(parsedContent);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/config.yaml', 'utf8');
|
|
expect(loadYaml).toHaveBeenCalledWith(fileContent);
|
|
expect(result).toEqual(parsedContent);
|
|
});
|
|
|
|
it('should load JavaScript files correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.js';
|
|
const moduleOutput = { settings: { temperature: 0.7 } };
|
|
|
|
vi.mocked(isJavascriptFile).mockReturnValue(true);
|
|
importModule.mockResolvedValue(moduleOutput);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(importModule).toHaveBeenCalledWith('/path/to/config.js', undefined);
|
|
expect(result).toEqual(moduleOutput);
|
|
});
|
|
|
|
it('should load JavaScript files with function name correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.js:getConfig';
|
|
const moduleOutput = { getConfig: 'success' };
|
|
|
|
vi.mocked(isJavascriptFile).mockReturnValue(true);
|
|
importModule.mockResolvedValue(moduleOutput);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(importModule).toHaveBeenCalledWith('/path/to/config.js', 'getConfig');
|
|
expect(result).toEqual(moduleOutput);
|
|
});
|
|
|
|
it('should load Python files correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.py';
|
|
const pythonOutput = { message: 'Hello from Python' };
|
|
|
|
runPython.mockResolvedValue(pythonOutput);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(runPython).toHaveBeenCalledWith('/path/to/config.py', 'get_config', []);
|
|
expect(result).toEqual(pythonOutput);
|
|
});
|
|
|
|
it('should load Python files with function name correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.py:custom_func';
|
|
const pythonOutput = { custom: true };
|
|
|
|
runPython.mockResolvedValue(pythonOutput);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(runPython).toHaveBeenCalledWith('/path/to/config.py', 'custom_func', []);
|
|
expect(result).toEqual(pythonOutput);
|
|
});
|
|
|
|
it('should load text files correctly', async () => {
|
|
const fileRef = 'file:///path/to/config.txt';
|
|
const fileContent = 'This is a text file';
|
|
|
|
readFileMock.mockResolvedValue(fileContent);
|
|
|
|
const result = await loadFileReference(fileRef);
|
|
|
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/config.txt', 'utf8');
|
|
expect(result).toEqual(fileContent);
|
|
});
|
|
|
|
it('should resolve file paths relative to the basePath', async () => {
|
|
const fileRef = 'file://config.json';
|
|
const basePath = '/base/path';
|
|
const fileContent = '{"name": "test"}';
|
|
const parsedContent = { name: 'test' };
|
|
|
|
vi.mocked(path.resolve).mockReturnValue('/base/path/config.json');
|
|
readFileMock.mockResolvedValue(fileContent);
|
|
vi.spyOn(JSON, 'parse').mockReturnValue(parsedContent);
|
|
|
|
const result = await loadFileReference(fileRef, basePath);
|
|
|
|
expect(path.resolve).toHaveBeenCalledWith('/base/path', 'config.json');
|
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/base/path/config.json', 'utf8');
|
|
expect(result).toEqual(parsedContent);
|
|
});
|
|
|
|
it('should throw an error for unsupported file types', async () => {
|
|
const fileRef = 'file:///path/to/file.xyz';
|
|
|
|
vi.mocked(path.extname).mockReturnValue('.xyz');
|
|
vi.mocked(isJavascriptFile).mockReturnValue(false);
|
|
|
|
await expect(loadFileReference(fileRef)).rejects.toThrow('Unsupported file extension: .xyz');
|
|
});
|
|
});
|
|
|
|
describe('processConfigFileReferences', () => {
|
|
it('should return primitive values as is', async () => {
|
|
await expect(processConfigFileReferences(42)).resolves.toBe(42);
|
|
await expect(processConfigFileReferences('test')).resolves.toBe('test');
|
|
await expect(processConfigFileReferences(true)).resolves.toBe(true);
|
|
await expect(processConfigFileReferences(null)).resolves.toBeNull();
|
|
await expect(processConfigFileReferences(undefined)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should process a simple file reference string', async () => {
|
|
const config = 'file:///path/to/config.json';
|
|
const parsedContent = { name: 'test', value: 42 };
|
|
|
|
readFileMock.mockResolvedValue(JSON.stringify(parsedContent));
|
|
vi.spyOn(JSON, 'parse').mockReturnValue(parsedContent);
|
|
|
|
const result = await processConfigFileReferences(config);
|
|
|
|
expect(result).toEqual(parsedContent);
|
|
});
|
|
|
|
it('should process nested file references in objects', async () => {
|
|
const config = {
|
|
setting1: 'value1',
|
|
setting2: 'file:///path/to/setting2.json',
|
|
nested: {
|
|
setting3: 'file:///path/to/setting3.yaml',
|
|
},
|
|
};
|
|
|
|
readFileMock.mockImplementation((path) => {
|
|
if (path === '/path/to/setting2.json') {
|
|
return Promise.resolve('{"key": "value2"}');
|
|
}
|
|
if (path === '/path/to/setting3.yaml') {
|
|
return Promise.resolve('key: value3');
|
|
}
|
|
return Promise.resolve('');
|
|
});
|
|
|
|
vi.spyOn(JSON, 'parse').mockImplementation((content) => {
|
|
if (content === '{"key": "value2"}') {
|
|
return { key: 'value2' };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
vi.mocked(loadYaml).mockImplementation((content) => {
|
|
if (content === 'key: value3') {
|
|
return { key: 'value3' };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
const result = await processConfigFileReferences(config);
|
|
|
|
expect(result).toEqual({
|
|
setting1: 'value1',
|
|
setting2: { key: 'value2' },
|
|
nested: {
|
|
setting3: { key: 'value3' },
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should process file references in arrays', async () => {
|
|
const config = [
|
|
'regular string',
|
|
'file:///path/to/item1.json',
|
|
'file:///path/to/item2.yaml',
|
|
42,
|
|
];
|
|
|
|
readFileMock.mockImplementation((path) => {
|
|
if (path === '/path/to/item1.json') {
|
|
return Promise.resolve('{"name": "item1"}');
|
|
}
|
|
if (path === '/path/to/item2.yaml') {
|
|
return Promise.resolve('name: item2');
|
|
}
|
|
return Promise.resolve('');
|
|
});
|
|
|
|
vi.spyOn(JSON, 'parse').mockImplementation((content) => {
|
|
if (content === '{"name": "item1"}') {
|
|
return { name: 'item1' };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
vi.mocked(loadYaml).mockImplementation((content) => {
|
|
if (content === 'name: item2') {
|
|
return { name: 'item2' };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
const result = await processConfigFileReferences(config);
|
|
|
|
expect(result).toEqual(['regular string', { name: 'item1' }, { name: 'item2' }, 42]);
|
|
});
|
|
|
|
it('should handle errors when processing file references', async () => {
|
|
const config = {
|
|
valid: 'regular value',
|
|
invalid: 'file:///path/to/nonexistent.json',
|
|
};
|
|
|
|
readFileMock.mockImplementation((path) => {
|
|
if (path === '/path/to/nonexistent.json') {
|
|
return Promise.reject(new Error('File not found'));
|
|
}
|
|
return Promise.resolve('');
|
|
});
|
|
|
|
await expect(processConfigFileReferences(config)).rejects.toThrow('File not found');
|
|
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('File not found'));
|
|
});
|
|
});
|
|
});
|