Files
promptfoo--promptfoo/test/providers/pythonCompletion.fileRef.test.ts
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

355 lines
10 KiB
TypeScript

import fsSync from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import logger from '../../src/logger';
import { PythonProvider } from '../../src/providers/pythonCompletion';
import * as pythonUtils from '../../src/python/pythonUtils';
import { processConfigFileReferences } from '../../src/util/fileReference';
import { parsePathOrGlob } from '../../src/util/index';
import type { Logger } from 'winston';
vi.mock('fs');
vi.mock('fs/promises');
vi.mock('path');
vi.mock('../../src/util/file');
vi.mock('../../src/logger');
vi.mock('../../src/util');
vi.mock('../../src/util/fileReference', async (importOriginal) => {
return {
...(await importOriginal()),
loadFileReference: vi.fn(),
processConfigFileReferences: vi.fn(),
};
});
const mocks = vi.hoisted(() => {
const mockPoolInstance = {
initialize: vi.fn().mockResolvedValue(undefined),
execute: vi.fn().mockResolvedValue({ output: 'Test output' }),
shutdown: vi.fn().mockResolvedValue(undefined),
getWorkerCount: vi.fn().mockReturnValue(1),
};
const PythonWorkerPoolMock = vi.fn(function () {
return mockPoolInstance as any;
});
const importModuleMock = vi.fn();
const isEsmModuleMock = vi.fn();
return { mockPoolInstance, PythonWorkerPoolMock, importModuleMock, isEsmModuleMock };
});
vi.mock('../../src/esm', async (importOriginal) => {
return {
...(await importOriginal()),
importModule: mocks.importModuleMock,
isEsmModule: mocks.isEsmModuleMock,
};
});
vi.mock('../../src/python/workerPool', async (importOriginal) => {
return {
...(await importOriginal()),
PythonWorkerPool: mocks.PythonWorkerPoolMock,
};
});
describe('PythonProvider with file references', () => {
const mockPoolInstance = mocks.mockPoolInstance;
const providers: PythonProvider[] = [];
beforeEach(() => {
vi.clearAllMocks();
// Reset mock pool
mocks.PythonWorkerPoolMock.mockClear();
mockPoolInstance.initialize.mockReset();
mockPoolInstance.initialize.mockResolvedValue(undefined);
mockPoolInstance.execute.mockReset();
mockPoolInstance.execute.mockResolvedValue({ output: 'Test output' });
mockPoolInstance.shutdown.mockReset();
mockPoolInstance.shutdown.mockResolvedValue(undefined);
mockPoolInstance.getWorkerCount.mockReset();
mockPoolInstance.getWorkerCount.mockReturnValue(1);
// Reset Python state to avoid test interference
pythonUtils.state.cachedPythonPath = null;
pythonUtils.state.validationPromise = null;
vi.mocked(logger.debug).mockImplementation(function () {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
});
vi.mocked(logger.error).mockImplementation(function () {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
});
vi.mocked(path.resolve).mockImplementation(function (...parts) {
return parts.join('/');
});
vi.mocked(path.relative).mockImplementation(function () {
return 'relative/path';
});
vi.mocked(path.join).mockImplementation(function (...parts) {
return parts.join('/');
});
vi.mocked(parsePathOrGlob).mockImplementation(function (_basePath, runPath) {
if (runPath.includes(':')) {
const [filePath, functionName] = runPath.split(':');
return {
filePath,
functionName,
isPathPattern: false,
extension: '.py',
};
}
return {
filePath: runPath,
functionName: undefined,
isPathPattern: false,
extension: '.py',
};
});
vi.mocked(fsSync.readFileSync).mockImplementation(function () {
return 'mock file content';
});
vi.mocked(fs.readFile).mockResolvedValue('mock file content');
});
afterEach(async () => {
// Cleanup providers
await Promise.all(providers.map((p) => p.shutdown().catch(() => {})));
providers.length = 0;
});
it('should call processConfigFileReferences when initializing config references', async () => {
const mockConfig = {
settings: 'file://settings.json',
templates: ['file://template1.yaml', 'file://template2.txt'],
};
const mockProcessedConfig = {
settings: { temperature: 0.7 },
templates: [{ prompt: 'Template 1' }, 'Template 2 content'],
};
vi.mocked(processConfigFileReferences).mockResolvedValue(mockProcessedConfig);
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
...mockConfig,
},
});
providers.push(provider);
await provider.initialize();
expect(processConfigFileReferences).toHaveBeenCalledWith(
expect.objectContaining(mockConfig),
'/base/path',
);
expect(provider.config).toEqual(mockProcessedConfig);
});
it('should handle errors during config reference processing', async () => {
const mockConfig = {
settings: 'file://settings.json',
basePath: '/base/path',
};
const mockError = new Error('Failed to load file');
vi.mocked(processConfigFileReferences).mockRejectedValue(mockError);
const provider = new PythonProvider('test.py', {
id: 'test',
config: mockConfig,
});
providers.push(provider);
await expect(provider.initialize()).rejects.toThrow('Failed to load file');
expect(processConfigFileReferences).toHaveBeenCalledWith(
expect.objectContaining(mockConfig),
expect.any(String),
);
expect(provider['isInitialized']).toBeFalsy();
});
it('should process config references before calling API', async () => {
const mockConfig = {
settings: 'file://settings.json',
};
const mockProcessedConfig = {
settings: { model: 'gpt-4' },
};
vi.mocked(processConfigFileReferences).mockResolvedValue(mockProcessedConfig);
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
...mockConfig,
},
});
providers.push(provider);
await provider.callApi('Test prompt');
expect(processConfigFileReferences).toHaveBeenCalledWith(
expect.objectContaining(mockConfig),
'/base/path',
);
expect(provider.config).toEqual(mockProcessedConfig);
});
it('should only process config references once', async () => {
const mockConfig = {
settings: 'file://settings.json',
};
vi.mocked(processConfigFileReferences).mockResolvedValue({
settings: { processed: true },
});
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
...mockConfig,
},
});
providers.push(provider);
await provider.initialize();
await provider.initialize();
await provider.initialize();
expect(processConfigFileReferences).toHaveBeenCalledTimes(1);
});
it('should pass the loaded config to python script execution', async () => {
const mockConfig = {
pythonExecutable: '/custom/python',
settings: 'file://settings.json',
};
const mockProcessedConfig = {
pythonExecutable: '/custom/python',
settings: { temperature: 0.8 },
};
vi.mocked(processConfigFileReferences).mockResolvedValue(mockProcessedConfig);
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
...mockConfig,
},
});
providers.push(provider);
await provider.callApi('Test prompt');
// Just verify config was processed - worker pool handles execution
expect(processConfigFileReferences).toHaveBeenCalledWith(
expect.objectContaining(mockConfig),
'/base/path',
);
expect(provider.config).toEqual(mockProcessedConfig);
});
it('should correctly handle integration with different API call types', async () => {
const mockProcessedConfig = {
pythonExecutable: '/custom/python',
};
vi.mocked(processConfigFileReferences).mockResolvedValue(mockProcessedConfig);
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
},
});
providers.push(provider);
vi.spyOn(provider, 'callApi').mockResolvedValue({
output: 'API result',
cached: false,
});
vi.spyOn(provider, 'callEmbeddingApi').mockResolvedValue({
embedding: [0.1, 0.2, 0.3],
});
vi.spyOn(provider, 'callClassificationApi').mockResolvedValue({
classification: { label: 0, score: 0.9 },
});
const apiResult = await provider.callApi('Test prompt');
const embeddingResult = await provider.callEmbeddingApi('Get embedding');
const classificationResult = await provider.callClassificationApi('Classify this');
expect(apiResult).toEqual({ output: 'API result', cached: false });
expect(embeddingResult).toEqual({ embedding: [0.1, 0.2, 0.3] });
expect(classificationResult).toEqual({ classification: { label: 0, score: 0.9 } });
});
it('should pass processed config to Python script instead of raw file references', async () => {
const mockOriginalConfig = {
settings: 'file://settings.json',
formats: 'file://formats.yaml',
};
const mockProcessedConfig = {
settings: { model: 'gpt-4', temperature: 0.7 },
formats: { outputFormat: 'json', includeTokens: true },
};
vi.mocked(processConfigFileReferences).mockResolvedValue(mockProcessedConfig);
const provider = new PythonProvider('test.py', {
id: 'test',
config: {
basePath: '/base/path',
...mockOriginalConfig,
},
});
providers.push(provider);
await provider.callApi('Test prompt');
// Verify that config was processed before execution
expect(processConfigFileReferences).toHaveBeenCalledWith(
expect.objectContaining(mockOriginalConfig),
'/base/path',
);
// Verify the processed config is stored on the provider
expect(provider.config.settings).toEqual(mockProcessedConfig.settings);
expect(provider.config.formats).toEqual(mockProcessedConfig.formats);
// Verify raw file references were not kept
expect(provider.config.settings).not.toBe('file://settings.json');
expect(provider.config.formats).not.toBe('file://formats.yaml');
});
});