chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
+379
View File
@@ -0,0 +1,379 @@
/**
* Smoke tests for advanced CLI features.
*
* These tests verify advanced evaluation features including:
* - Environment file loading (--env-file)
* - Delay between tests (--delay)
* - HTML output format
* - Config loading from separate files
* - Multiple prompts (prompt comparison/A-B testing)
* - icontains assertion (case-insensitive)
* - regex assertion with end-of-string pattern
* - Multiple file prompts (file:// references)
* - Assertion weights
* - Test threshold option
*
* @see docs/plans/smoke-tests.md for the full checklist
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Path to the built CLI binary
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output-advanced');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; expectError?: boolean; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 60000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
describe('Advanced Features Smoke Tests', () => {
beforeAll(() => {
// Verify the built binary exists
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
// Create output directory for test artifacts
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
// Clean up output directory
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('1.4.8 Environment File Loading', () => {
it('1.4.8 - --env-file loads environment variables', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/env-var-test.yaml');
const envFilePath = path.join(FIXTURES_DIR, 'data/test.env');
const outputPath = path.join(OUTPUT_DIR, 'env-test-output.json');
const { exitCode } = runCli([
'eval',
'-c',
configPath,
'--env-file',
envFilePath,
'-o',
outputPath,
'--no-cache',
]);
expect(exitCode).toBe(0);
// Verify the env vars were substituted in the output
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const output = parsed.results.results[0].response.output;
// The env vars should be present in the echoed output
expect(output).toContain('sk-test-12345');
expect(output).toContain('super-secret-value');
});
it('1.4.8b - --env-file resolves env placeholders in file:// provider references', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/file-provider-env-7079.yaml');
const envFilePath = path.join(FIXTURES_DIR, 'data/file-provider-env-7079.env');
const outputPath = path.join(OUTPUT_DIR, 'file-provider-env-7079-output.json');
const { exitCode } = runCli([
'eval',
'-c',
configPath,
'--env-file',
envFilePath,
'-o',
outputPath,
'--no-cache',
]);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
expect(result.provider.id).toBe('echo');
expect(result.provider.label).toBe('Smoke Provider Label');
expect(result.response.output).toContain('Hello World');
});
});
describe('2.5.2 Conversation Relevance Config Loading', () => {
it('2.5.2 - preserves template syntax in static _conversation vars', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/conversation-relevance-ssti.yaml');
const outputPath = path.join(OUTPUT_DIR, 'conversation-relevance-ssti-output.json');
const capturePath = path.join(OUTPUT_DIR, 'conversation-relevance-ssti-prompt.txt');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
env: {
OPENAI_API_KEY: 'SHOULD_NOT_RENDER',
PROMPTFOO_CAPTURE_PATH: capturePath,
},
});
expect(exitCode).toBe(0);
const prompt = fs.readFileSync(capturePath, 'utf-8');
expect(prompt).toContain('What is the capital of {{country}}?');
expect(prompt).toContain('{{capital}}');
expect(prompt).toContain('{{ env.OPENAI_API_KEY }}');
expect(prompt).toContain('{% for x in range(3) %}{{x}}{% endfor %}');
expect(prompt).not.toContain('SHOULD_NOT_RENDER');
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
});
describe('1.10.1 Delay Between Tests', () => {
it('1.10.1 - --delay adds delay between tests', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/delay-test.yaml');
// Run with 100ms delay between 3 tests
const startTime = Date.now();
const { exitCode } = runCli(['eval', '-c', configPath, '--delay', '100', '--no-cache']);
const endTime = Date.now();
expect(exitCode).toBe(0);
// With 3 tests and 100ms delay, total time should be at least 200ms (2 delays)
// Being generous with timing to avoid flaky tests
const elapsed = endTime - startTime;
expect(elapsed).toBeGreaterThanOrEqual(150); // At least some delay occurred
});
});
describe('1.4.4b HTML Output Format', () => {
it('1.4.4b - outputs HTML format', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'output.html');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
expect(fs.existsSync(outputPath)).toBe(true);
// Verify it's HTML content (lowercase doctype is valid HTML5)
const content = fs.readFileSync(outputPath, 'utf-8');
expect(content).toContain('<!doctype html>');
expect(content).toContain('<html');
});
});
describe('2.5 Config Loading', () => {
it('2.5.1 - loads separate config file correctly', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/config-tests.yaml');
const outputPath = path.join(OUTPUT_DIR, 'config-load-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Verify the config loaded and ran correctly
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const output = parsed.results.results[0].response.output;
expect(output).toContain('ConfigMergeTest');
});
});
describe('4.3.1b Multiple Prompts', () => {
it('4.3.1b - evaluates multiple prompts against same tests', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/multi-prompt.yaml');
const outputPath = path.join(OUTPUT_DIR, 'multi-prompt-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Verify we have results for all 3 prompts
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 3 prompts
expect(parsed.results.prompts.length).toBe(3);
// Should have 3 results (one test case x 3 prompts)
expect(parsed.results.results.length).toBe(3);
// Each prompt should produce different output
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('Hello'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Hi'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Hey'))).toBe(true);
});
});
describe('5.1.2b icontains Assertion', () => {
it('5.1.2b - icontains performs case-insensitive match', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/icontains-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'icontains-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// All case-insensitive assertions should pass
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
// Should have 3 assertions all passing
expect(parsed.results.results[0].gradingResult.componentResults.length).toBe(3);
expect(
parsed.results.results[0].gradingResult.componentResults.every(
(r: { pass: boolean }) => r.pass,
),
).toBe(true);
});
});
describe('5.1.5b Regex End-of-String Pattern', () => {
it('5.1.5b - regex with $ anchor checks string suffix', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/ends-with-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'regex-ends-with-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Both regex end-of-string assertions should pass
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
expect(parsed.results.results[0].gradingResult.componentResults.length).toBe(2);
});
});
describe('4.3.2b Multiple File Prompts', () => {
it('4.3.2b - file:// references load multiple prompt files', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/glob-prompts.yaml');
const outputPath = path.join(OUTPUT_DIR, 'file-prompts-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Should load both prompt files from file:// references
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 2 prompts (greeting.txt and farewell.txt)
expect(parsed.results.prompts.length).toBe(2);
// Should have 2 results (1 test x 2 prompts)
expect(parsed.results.results.length).toBe(2);
// Verify both prompts were used
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('welcome'))).toBe(true);
expect(outputs.some((o: string) => o.includes('see you'))).toBe(true);
});
});
describe('5.3 Assertion Weights', () => {
it('5.3.1 - assertion weights affect scoring', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/weighted-assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'weighted-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Both assertions pass, so test passes
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
// The weighted assertions should both pass
expect(parsed.results.results[0].gradingResult.componentResults.length).toBe(2);
});
it('5.3.2 - weighted named metrics preserve prompt totals and denominators', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/weighted-named-metric.yaml');
const outputPath = path.join(OUTPUT_DIR, 'weighted-named-metric-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
expect(parsed.results.results[0].score).toBeCloseTo(0.75, 10);
expect(parsed.results.results[0].gradingResult.namedScores.weightedMetric).toBeCloseTo(
0.75,
10,
);
expect(parsed.results.results[0].gradingResult.namedScoreWeights.weightedMetric).toBe(4);
expect(parsed.results.prompts[0].metrics.namedScores.weightedMetric).toBeCloseTo(3, 10);
expect(parsed.results.prompts[0].metrics.namedScoresCount.weightedMetric).toBe(2);
expect(parsed.results.prompts[0].metrics.namedScoreWeights.weightedMetric).toBe(4);
expect(
parsed.results.prompts[0].metrics.namedScores.weightedMetric /
parsed.results.prompts[0].metrics.namedScoreWeights.weightedMetric,
).toBeCloseTo(0.75, 10);
});
});
describe('7.4 Test Threshold', () => {
it('7.4.1 - test threshold allows partial assertion passes', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/test-threshold.yaml');
const outputPath = path.join(OUTPUT_DIR, 'threshold-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
// The test should pass because 2/3 assertions pass (66%) > threshold (50%)
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Test should pass due to threshold
expect(parsed.results.results[0].success).toBe(true);
// Verify assertion results
const componentResults = parsed.results.results[0].gradingResult.componentResults;
expect(componentResults.length).toBe(3);
// 2 should pass, 1 should fail
const passCount = componentResults.filter((r: { pass: boolean }) => r.pass).length;
const failCount = componentResults.filter((r: { pass: boolean }) => !r.pass).length;
expect(passCount).toBe(2);
expect(failCount).toBe(1);
});
});
});
+438
View File
@@ -0,0 +1,438 @@
/**
* Smoke tests for CLI binary operations.
*
* These tests verify the built CLI binary works correctly.
* They run against dist/src/main.js (the built package).
*
* @see docs/plans/smoke-tests.md for the full checklist
*/
import { spawn, spawnSync } from 'child_process';
import * as fs from 'fs';
import { createServer, type Server as HttpServer } from 'http';
import * as path from 'path';
import { Server as SocketIOServer } from 'socket.io';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Path to the built CLI binaries
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ENTRYPOINT_PATH = path.resolve(__dirname, '../../dist/src/entrypoint.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; expectError?: boolean; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 30000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
/**
* Helper to run the CLI via entrypoint.js (the actual bin entry)
*/
function runEntrypoint(
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [ENTRYPOINT_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 30000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
function runEntrypointAsync(
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
return new Promise((resolve, reject) => {
const child = spawn('node', [ENTRYPOINT_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
env: { ...process.env, ...options.env, NO_COLOR: '1' },
});
let stdout = '';
let stderr = '';
const timeoutId = setTimeout(() => {
child.kill('SIGTERM');
reject(new Error(`CLI entrypoint timed out for: ${args.join(' ')}`));
}, 30000);
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => {
stdout += chunk;
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => {
stderr += chunk;
});
child.once('error', (error) => {
clearTimeout(timeoutId);
reject(error);
});
child.once('close', (code) => {
clearTimeout(timeoutId);
resolve({
stdout,
stderr,
exitCode: code ?? 1,
});
});
});
}
function runGit(args: string[], cwd: string): void {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf-8',
});
if (result.status !== 0) {
throw new Error(
`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`,
);
}
}
describe('CLI Smoke Tests', () => {
beforeAll(() => {
// Verify the built binaries exist
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
if (!fs.existsSync(ENTRYPOINT_PATH)) {
throw new Error(
`Built entrypoint not found at ${ENTRYPOINT_PATH}. Run 'npm run build' first.`,
);
}
});
describe('1.1 Basic CLI Operations', () => {
it('1.1.1 - outputs version with --version', () => {
const { stdout, exitCode } = runCli(['--version']);
expect(exitCode).toBe(0);
// Version should contain semver pattern (e.g., 1.2.3 or 1.2.3-beta.1)
// Note: Output may include warnings if invalid config files exist in cwd
expect(stdout).toMatch(/\d+\.\d+\.\d+(-[\w.]+)?/);
});
it('1.1.2 - outputs help with --help', () => {
const { stdout, exitCode } = runCli(['--help']);
expect(exitCode).toBe(0);
expect(stdout).toContain('promptfoo');
expect(stdout).toContain('eval');
expect(stdout).toContain('Commands:');
});
it('1.1.3 - outputs subcommand help with eval --help', () => {
const { stdout, exitCode } = runCli(['eval', '--help']);
expect(exitCode).toBe(0);
expect(stdout).toContain('--config');
expect(stdout).toContain('--output');
expect(stdout).toContain('--no-cache');
});
it('1.1.4 - handles unknown command gracefully', () => {
const { stderr, exitCode } = runCli(['unknowncommand123']);
expect(exitCode).toBe(1);
// Commander outputs "error: unknown command" (lowercase)
expect(stderr.toLowerCase()).toContain('unknown command');
});
it('1.1.5 - handles missing config file gracefully', () => {
const { stdout, stderr, exitCode } = runCli(['eval', '-c', 'nonexistent-config-file.yaml']);
expect(exitCode).toBe(1);
// Error may appear in stdout (uncaught exception) or stderr
const output = stdout + stderr;
// Should indicate the file wasn't found
expect(output.toLowerCase()).toMatch(
/not found|no such file|does not exist|cannot find|no configuration file/i,
);
});
});
describe('1.2 Init Command', () => {
let tempDir: string;
beforeAll(() => {
// Create a temp directory for init tests
tempDir = path.join(ROOT_DIR, 'test/smoke/.temp-init-test');
fs.mkdirSync(tempDir, { recursive: true });
});
afterAll(() => {
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it('1.2.1 - init --no-interactive creates config file', () => {
const initDir = path.join(tempDir, 'init-test');
fs.mkdirSync(initDir, { recursive: true });
const { exitCode } = runCli(['init', '--no-interactive'], { cwd: initDir });
expect(exitCode).toBe(0);
// Should create a promptfooconfig file
const configExists =
fs.existsSync(path.join(initDir, 'promptfooconfig.yaml')) ||
fs.existsSync(path.join(initDir, 'promptfooconfig.yml'));
expect(configExists).toBe(true);
});
});
describe('1.3 Validate Command', () => {
const fixturesDir = path.join(__dirname, 'fixtures/configs');
it('1.3.1 - validates a correct config file', () => {
const { stdout, exitCode } = runCli(['validate', '-c', path.join(fixturesDir, 'basic.yaml')]);
expect(exitCode).toBe(0);
expect(stdout.toLowerCase()).toContain('valid');
});
it('1.3.2 - rejects an invalid config file', () => {
const { stdout, stderr, exitCode } = runCli([
'validate',
'-c',
path.join(fixturesDir, 'invalid.yaml'),
]);
expect(exitCode).toBe(1);
// Error may appear in stdout or stderr
const output = stdout + stderr;
expect(output.length).toBeGreaterThan(0);
// Should indicate a validation error (missing providers)
expect(output.toLowerCase()).toMatch(/provider|invalid|error/i);
});
});
describe('1.6 Cache Commands', () => {
it('1.6.1 - cache clear executes without error', () => {
const { exitCode } = runCli(['cache', 'clear']);
// Should succeed (exit 0) even if cache is empty
expect(exitCode).toBe(0);
});
});
describe('1.5 List Commands', () => {
it('1.5.1 - list evals executes without error', () => {
const { exitCode } = runCli(['list', 'evals']);
// Should succeed even if no evals exist
expect(exitCode).toBe(0);
});
it('1.5.2 - list datasets executes without error', () => {
const { exitCode } = runCli(['list', 'datasets']);
// Should succeed even if no datasets exist
expect(exitCode).toBe(0);
});
});
describe('1.7 Entrypoint Wrapper', () => {
/**
* These tests verify that entrypoint.js (the actual bin entry) works correctly.
* The entrypoint provides a Node.js version check before loading dependencies.
*/
it('1.7.1 - entrypoint outputs version with --version', () => {
const { stdout, exitCode } = runEntrypoint(['--version']);
expect(exitCode).toBe(0);
// Version should contain semver pattern
expect(stdout).toMatch(/\d+\.\d+\.\d+(-[\w.]+)?/);
});
it('1.7.2 - entrypoint outputs help with --help', () => {
const { stdout, exitCode } = runEntrypoint(['--help']);
expect(exitCode).toBe(0);
expect(stdout).toContain('promptfoo');
expect(stdout).toContain('eval');
expect(stdout).toContain('Commands:');
});
it('1.7.3 - entrypoint handles subcommands correctly', () => {
const { stdout, exitCode } = runEntrypoint(['eval', '--help']);
expect(exitCode).toBe(0);
expect(stdout).toContain('--config');
expect(stdout).toContain('--output');
});
it('1.7.4 - entrypoint produces identical output to main.js', () => {
// Verify entrypoint delegates to main.js correctly
const entrypointResult = runEntrypoint(['--version']);
const mainResult = runCli(['--version']);
expect(entrypointResult.exitCode).toBe(mainResult.exitCode);
expect(entrypointResult.stdout.trim()).toBe(mainResult.stdout.trim());
});
});
describe('1.8 Code Scan Structured Output', () => {
let repoDir: string;
let httpServer: HttpServer;
let socketServer: SocketIOServer;
let apiHost: string;
beforeAll(async () => {
repoDir = path.join(ROOT_DIR, 'test/smoke/.temp-code-scan-structured-output');
fs.rmSync(repoDir, { recursive: true, force: true });
fs.mkdirSync(repoDir, { recursive: true });
runGit(['init', '-b', 'main'], repoDir);
runGit(['config', 'user.email', 'smoke@example.com'], repoDir);
runGit(['config', 'user.name', 'Promptfoo Smoke Test'], repoDir);
runGit(['commit', '--allow-empty', '-m', 'baseline'], repoDir);
httpServer = createServer((_request, response) => {
response.writeHead(200, { 'content-type': 'text/plain' });
response.end('ok');
});
socketServer = new SocketIOServer(httpServer, {
cors: { origin: '*' },
});
socketServer.on('connection', (socket) => {
socket.on('agent:join', () => {});
});
await new Promise<void>((resolve, reject) => {
httpServer.once('error', reject);
httpServer.listen(0, '127.0.0.1', () => {
httpServer.off('error', reject);
const address = httpServer.address();
if (!address || typeof address === 'string') {
reject(new Error('Failed to resolve mock code-scan server port'));
return;
}
apiHost = `http://127.0.0.1:${address.port}`;
resolve();
});
});
});
afterAll(async () => {
await new Promise<void>((resolve) => {
socketServer.close(() => {
httpServer.close(() => resolve());
});
});
fs.rmSync(repoDir, { recursive: true, force: true });
});
it.each([
[
'JSON',
['--json'],
(payload: Record<string, unknown>) => {
expect(payload).toMatchObject({
success: true,
comments: [],
review: 'No files to scan',
});
},
],
[
'SARIF',
['--format', 'sarif'],
(payload: Record<string, unknown>) => {
expect(payload).toMatchObject({
version: '2.1.0',
runs: [expect.any(Object)],
});
},
],
])('1.8.%# - keeps %s stdout machine-readable under --verbose and LOG_LEVEL=debug', async (_label, outputArgs, assertPayload) => {
const { stdout, stderr, exitCode } = await runEntrypointAsync(
[
'code-scans',
'run',
repoDir,
'--diffs-only',
'--api-host',
apiHost,
'--base',
'main',
'--compare',
'HEAD',
...outputArgs,
'--verbose',
],
{
env: {
LOG_LEVEL: 'debug',
NODE_NO_WARNINGS: '1',
PROMPTFOO_DISABLE_UPDATE: 'true',
},
},
);
expect(exitCode).toBe(0);
expect(stderr).toBe('');
const payload = JSON.parse(stdout) as Record<string, unknown>;
assertPayload(payload);
});
it('1.8.2 - keeps structured config failures off stdout', async () => {
const missingConfigPath = path.join(repoDir, 'definitely-missing-code-scan-config.yaml');
const { stdout, stderr, exitCode } = await runEntrypointAsync(
['code-scans', 'run', repoDir, '--json', '--config', missingConfigPath],
{
env: {
NODE_NO_WARNINGS: '1',
},
},
);
expect(exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain(`Scan failed: Configuration file not found: ${missingConfigPath}`);
});
it('1.8.3 - keeps structured output flag conflicts off stdout', async () => {
const { stdout, stderr, exitCode } = await runEntrypointAsync(
['code-scans', 'run', repoDir, '--json', '--format', 'sarif'],
{
env: {
NODE_NO_WARNINGS: '1',
},
},
);
expect(exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain('Scan failed: Cannot combine --json with --format sarif');
});
});
});
+398
View File
@@ -0,0 +1,398 @@
/**
* Smoke tests for config formats, providers, and data loading.
*
* These tests verify various configuration formats, provider types,
* and data loading mechanisms work correctly.
*
* @see docs/plans/smoke-tests.md for the full checklist
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { getOpenAiMissingApiKeyMessage } from '../providers/openai/shared';
// Path to the built CLI binary
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output-configs');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 60000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
describe('Config Format Smoke Tests', () => {
beforeAll(() => {
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('2.2 JSON Configs', () => {
it('2.2.1 - parses JSON config format', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.json');
const outputPath = path.join(OUTPUT_DIR, 'json-config-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Verify the eval ran correctly
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
});
});
describe('Provider Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('3.1 Built-in Providers', () => {
it('3.1.2 - exec provider executes shell commands', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/exec-provider.yaml');
const outputPath = path.join(OUTPUT_DIR, 'exec-provider-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
// Exec provider should have executed the echo command
expect(result.response.output).toContain('Echo from exec');
});
// Regression for #8686: exec provider must close the child's stdin so that
// scripts reading from stdin (e.g. opencode) do not hang forever.
it('3.1.3 - exec provider closes stdin on child process', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/exec-provider-stdin.yaml');
const outputPath = path.join(OUTPUT_DIR, 'exec-provider-stdin-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
expect(result.success).toBe(true);
expect(result.response.output).toContain('stdin closed');
});
});
describe('3.3 OpenAI Providers', () => {
it('3.3.1 - passes tools and tool_choice to providers', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/normalized-tools.yaml');
const outputPath = path.join(OUTPUT_DIR, 'normalized-tools-output.json');
// Run from configs directory so relative script paths work
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: path.join(FIXTURES_DIR, 'configs'),
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have results for all 3 providers
expect(parsed.results.results.length).toBe(3);
// All results should be successful
parsed.results.results.forEach((result: { success: boolean }) => {
expect(result.success).toBe(true);
});
// Verify tool_choice modes are passed correctly to each provider
const results = parsed.results.results;
const autoResult = results.find(
(r: { provider: { label: string } }) => r.provider.label === 'Auto tool choice',
);
const autoOutput = JSON.parse(autoResult.response.output);
expect(autoOutput.tool_choice).toBe('auto');
const requiredResult = results.find(
(r: { provider: { label: string } }) => r.provider.label === 'Required tool choice',
);
const requiredOutput = JSON.parse(requiredResult.response.output);
expect(requiredOutput.tool_choice).toBe('required');
const specificResult = results.find(
(r: { provider: { label: string } }) => r.provider.label === 'Specific tool choice',
);
const specificOutput = JSON.parse(specificResult.response.output);
expect(specificOutput.tool_choice.type).toBe('function');
expect(specificOutput.tool_choice.function.name).toBe('get_weather');
});
it('3.3.2 - surfaces custom apiKeyEnvar in missing API key errors', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/openai-custom-api-key-envar.yaml');
const outputPath = path.join(OUTPUT_DIR, 'openai-custom-api-key-envar-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
env: {
OPENAI_API_KEY: '',
CUSTOM_ASSISTANT_KEY: '',
},
});
expect(exitCode).toBe(100);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
expect(result.success).toBe(false);
expect(result.error).toContain(getOpenAiMissingApiKeyMessage('CUSTOM_ASSISTANT_KEY'));
expect(result.error).not.toContain(getOpenAiMissingApiKeyMessage());
});
});
describe('3.4 Python Providers', () => {
it('3.4.1 - Python provider with default call_api function', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/python-provider.yaml');
const outputPath = path.join(OUTPUT_DIR, 'python-provider-output.json');
// Run from the configs directory so relative paths work
const { exitCode, stdout, stderr } = runCli(
['eval', '-c', configPath, '-o', outputPath, '--no-cache'],
{ cwd: path.join(FIXTURES_DIR, 'configs') },
);
// Python provider requires Python to be installed
// If Python is not available, this test will fail gracefully
if (exitCode !== 0) {
const output = stdout + stderr;
if (output.includes('python') || output.includes('Python')) {
return;
}
}
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
expect(result.response.output).toContain('Python Echo:');
});
});
});
describe('Data Loading Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('4.2 Tests Loading', () => {
it('4.2.2 - loads tests from CSV file', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/csv-tests.yaml');
const outputPath = path.join(OUTPUT_DIR, 'csv-tests-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: path.join(FIXTURES_DIR, 'configs'),
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 2 test results (Alice and Bob from CSV)
expect(parsed.results.results.length).toBe(2);
// Verify the vars were loaded correctly
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('Alice'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Bob'))).toBe(true);
});
it('4.2.3 - loads tests from JSON file', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/json-tests.yaml');
const outputPath = path.join(OUTPUT_DIR, 'json-tests-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: path.join(FIXTURES_DIR, 'configs'),
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 2 test results (Charlie and Diana from JSON)
expect(parsed.results.results.length).toBe(2);
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('Charlie'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Diana'))).toBe(true);
});
});
describe('4.3 Prompts Loading', () => {
it('4.3.2 - loads prompts from file:// reference', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/file-prompt.yaml');
const outputPath = path.join(OUTPUT_DIR, 'file-prompt-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: path.join(FIXTURES_DIR, 'configs'),
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const result = parsed.results.results[0];
// The prompt should have been loaded from the file and rendered
expect(result.response.output).toContain('bananas');
});
});
});
describe('Assertion Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('5.1 Built-in Assertions', () => {
it('5.1.3, 5.1.5, 5.1.6, 5.2.1 - various assertion types work correctly', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'assertions-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// All 6 tests should pass
expect(parsed.results.results.length).toBe(6);
for (const result of parsed.results.results) {
expect(result.success).toBe(true);
}
});
it('5.1.3 - equals assertion matches exactly', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'equals-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// First test uses equals assertion
const equalsResult = parsed.results.results[0];
expect(equalsResult.success).toBe(true);
expect(equalsResult.response.output).toBe('exact match test');
});
it('5.1.5 - regex assertion matches patterns', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'regex-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Second test uses regex assertion
const regexResult = parsed.results.results[1];
expect(regexResult.success).toBe(true);
});
it('5.1.6 - is-json assertion validates JSON', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'isjson-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Fourth test uses is-json assertion
const jsonResult = parsed.results.results[3];
expect(jsonResult.success).toBe(true);
});
it('5.2.1 - inline JavaScript assertion executes correctly', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/assertions.yaml');
const outputPath = path.join(OUTPUT_DIR, 'js-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Sixth test uses JavaScript assertion
const jsResult = parsed.results.results[5];
expect(jsResult.success).toBe(true);
});
});
});
+211
View File
@@ -0,0 +1,211 @@
/**
* Smoke tests for the eval command.
*
* These tests verify the core evaluation pipeline works correctly
* using the echo provider (no external API dependencies).
*
* @see docs/plans/smoke-tests.md for the full checklist
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Path to the built CLI binary
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; expectError?: boolean; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 60000, // Eval can take longer
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
describe('Eval Smoke Tests', () => {
beforeAll(() => {
// Verify the built binary exists
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
// Create output directory for test artifacts
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
// Clean up output directory
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('1.4 Eval Command', () => {
it('1.4.1 - runs basic eval with echo provider', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const { stdout, exitCode } = runCli(['eval', '-c', configPath, '--no-cache']);
// Should complete successfully
expect(exitCode).toBe(0);
// Should show eval results
expect(stdout).toContain('PASS');
});
it('1.4.2 - outputs JSON format', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
expect(fs.existsSync(outputPath)).toBe(true);
// Verify it's valid JSON with expected structure
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed).toHaveProperty('results');
expect(parsed.results).toHaveProperty('results');
expect(Array.isArray(parsed.results.results)).toBe(true);
});
it('1.4.3 - outputs YAML format', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'output.yaml');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
expect(fs.existsSync(outputPath)).toBe(true);
// Verify it contains YAML-like content
const content = fs.readFileSync(outputPath, 'utf-8');
expect(content).toContain('results:');
});
it('1.4.4 - outputs CSV format', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'output.csv');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
expect(fs.existsSync(outputPath)).toBe(true);
// Verify it's CSV format (has header row with columns)
const content = fs.readFileSync(outputPath, 'utf-8');
const lines = content.trim().split('\n');
expect(lines.length).toBeGreaterThan(0);
// CSV should have comma-separated values
expect(lines[0]).toContain(',');
});
it('1.4.5 - respects --max-concurrency flag', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const { exitCode } = runCli([
'eval',
'-c',
configPath,
'--max-concurrency',
'1',
'--no-cache',
]);
// Should complete successfully with concurrency limit
expect(exitCode).toBe(0);
});
it('1.4.6 - respects --repeat flag', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'repeat-output.json');
const { exitCode } = runCli([
'eval',
'-c',
configPath,
'--repeat',
'2',
'-o',
outputPath,
'--no-cache',
]);
expect(exitCode).toBe(0);
// Verify we got repeated results
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// With repeat=2 and 1 test case, we should have 2 results
expect(parsed.results.results.length).toBe(2);
});
it('1.4.7 - runs with --verbose flag', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const { stdout, exitCode } = runCli(['eval', '-c', configPath, '--verbose', '--no-cache']);
expect(exitCode).toBe(0);
// Verbose mode should produce more output
expect(stdout.length).toBeGreaterThan(0);
});
});
describe('1.7 Exit Codes', () => {
it('1.7.1 - returns exit code 0 when all assertions pass', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const { exitCode } = runCli(['eval', '-c', configPath, '--no-cache']);
expect(exitCode).toBe(0);
});
it('1.7.2 - returns exit code 100 when assertions fail', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/failing-assertion.yaml');
const { exitCode } = runCli(['eval', '-c', configPath, '--no-cache']);
// Exit code 100 indicates test failures
expect(exitCode).toBe(100);
});
it('1.7.3 - returns exit code 1 for config errors', () => {
const { exitCode } = runCli(['eval', '-c', 'nonexistent-file.yaml', '--no-cache']);
expect(exitCode).toBe(1);
});
});
describe('Built-in Providers', () => {
it('3.1.1 - echo provider works correctly', () => {
const configPath = path.join(FIXTURES_DIR, 'configs/basic.yaml');
const outputPath = path.join(OUTPUT_DIR, 'echo-test.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache']);
expect(exitCode).toBe(0);
// Verify echo provider returns the prompt
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
const firstResult = parsed.results.results[0];
// Echo provider should return the prompt in the response
expect(firstResult.response.output).toContain('Hello');
expect(firstResult.response.output).toContain('World');
});
});
});
+129
View File
@@ -0,0 +1,129 @@
/**
* Smoke coverage for portable eval export/import parity.
*
* The CLI smoke suite has a 30 second per-test budget, so this exercise keeps
* import and export in one process while still going through the real command
* actions, database migration path, and JSON output writer.
*/
import fs from 'fs';
import path from 'path';
import { Command } from 'commander';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { exportCommand } from '../../src/commands/export';
import { importCommand } from '../../src/commands/import';
import { closeDbIfOpen } from '../../src/database/index';
import { runDbMigrations } from '../../src/migrate';
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output-export-import');
const SAMPLE_EXPORT_PATH = path.resolve(__dirname, '../__fixtures__/sample-export.json');
interface PortableEvalExport {
evalId: string;
config: Record<string, unknown>;
vars?: string[];
runtimeOptions?: Record<string, unknown>;
metadata: {
evaluationCreatedAt: string;
};
results: {
prompts: Array<{
id: string;
label: string;
provider: string;
raw: string;
}>;
results: Array<{
response?: { output?: unknown };
score: number;
success: boolean;
vars?: Record<string, unknown>;
}>;
stats: {
durationMs?: number;
evaluationDurationMs?: number;
errors: number;
failures: number;
generationDurationMs?: number;
successes: number;
};
};
}
function parityFields(output: PortableEvalExport) {
return {
evalId: output.evalId,
config: output.config,
vars: output.vars,
runtimeOptions: output.runtimeOptions,
evaluationCreatedAt: output.metadata.evaluationCreatedAt,
prompts: output.results.prompts.map(({ id, label, provider, raw }) => ({
id,
label,
provider,
raw,
})),
results: output.results.results.map(({ response, score, success, vars }) => ({
output: response?.output,
score,
success,
vars,
})),
stats: {
durationMs: output.results.stats.durationMs,
evaluationDurationMs: output.results.stats.evaluationDurationMs,
errors: output.results.stats.errors,
failures: output.results.stats.failures,
generationDurationMs: output.results.stats.generationDurationMs,
successes: output.results.stats.successes,
},
};
}
describe('Export/import smoke tests', () => {
beforeAll(async () => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
await runDbMigrations();
});
afterAll(async () => {
await closeDbIfOpen();
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
process.exitCode = undefined;
});
it('round-trips a portable eval through import and export', async () => {
process.exitCode = undefined;
const importPath = path.join(OUTPUT_DIR, 'portable-eval.json');
const reexportPath = path.join(OUTPUT_DIR, 'reexported-eval.json');
const exportData = JSON.parse(
fs.readFileSync(SAMPLE_EXPORT_PATH, 'utf-8'),
) as PortableEvalExport;
exportData.evalId = 'eval-smoke-export-import-cycle';
exportData.vars = ['riddle', 'unused_export_var'];
exportData.runtimeOptions = { cache: false, maxConcurrency: 2, repeat: 3 };
fs.writeFileSync(importPath, JSON.stringify(exportData));
const importProgram = new Command();
importCommand(importProgram);
await importProgram.parseAsync(['node', 'smoke', 'import', importPath]);
expect(process.exitCode).toBeUndefined();
const exportProgram = new Command();
exportCommand(exportProgram);
await exportProgram.parseAsync([
'node',
'smoke',
'export',
'eval',
exportData.evalId,
'--output',
reexportPath,
]);
expect(process.exitCode).toBeUndefined();
const reexportData = JSON.parse(fs.readFileSync(reexportPath, 'utf-8')) as PortableEvalExport;
expect(parityFields(reexportData)).toEqual(parityFields(exportData));
});
});
+121
View File
@@ -0,0 +1,121 @@
/**
* Smoke tests for extension hooks.
*
* Verifies that a Python extension hook returning its context does not break
* function-based prompts — the hook's subprocess JSON round-trip used to drop
* the non-serializable prompt function, sending raw Python source to the
* provider instead (regression test for
* https://github.com/promptfoo/promptfoo/issues/9653).
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Path to the built CLI binary
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output-extension-hooks');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 60000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
function findPythonPath(): string | undefined {
const candidates: Array<[string, string[]]> = [];
if (process.env.PROMPTFOO_PYTHON) {
candidates.push([process.env.PROMPTFOO_PYTHON, ['-c', 'import sys; print(sys.executable)']]);
}
if (process.platform === 'win32') {
candidates.push(['py', ['-3', '-c', 'import sys; print(sys.executable)']]);
}
candidates.push(
['python3', ['-c', 'import sys; print(sys.executable)']],
['python', ['-c', 'import sys; print(sys.executable)']],
);
for (const [command, args] of candidates) {
const result = spawnSync(command, args, { encoding: 'utf-8', timeout: 5000 });
if (result.status === 0 && result.stdout.trim()) {
return result.stdout.trim();
}
}
return undefined;
}
const PYTHON_PATH = findPythonPath();
describe('Extension Hook Smoke Tests', () => {
beforeAll(() => {
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
});
it.skipIf(!PYTHON_PATH)(
'executes a Python prompt function when a Python extension hook returns its context (issue #9653)',
() => {
const configPath = path.join(FIXTURES_DIR, 'configs/python-prompt-with-extension.yaml');
const outputPath = path.join(OUTPUT_DIR, 'python-prompt-with-extension-output.json');
const { exitCode } = runCli(
[
'eval',
'-c',
configPath,
'-o',
outputPath,
'--no-cache',
'--no-share',
'--no-table',
'--no-progress-bar',
],
{
cwd: path.join(FIXTURES_DIR, 'configs'),
env: {
PROMPTFOO_CONFIG_DIR: OUTPUT_DIR,
PROMPTFOO_PYTHON: PYTHON_PATH,
PROMPTFOO_DISABLE_SHARING: 'true',
PROMPTFOO_DISABLE_TELEMETRY: 'true',
PROMPTFOO_DISABLE_UPDATE: 'true',
},
},
);
expect(exitCode).toBe(0);
const parsed = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
const result = parsed.results.results[0];
// The executed prompt function produces chat messages. If the hook's JSON
// round-trip had dropped the function, the raw Python source would have
// been rendered into the prompt instead.
expect(result.prompt.raw).toContain('What is Linear Algebra?');
expect(result.prompt.raw).not.toContain('def create_prompt');
},
);
});
+309
View File
@@ -0,0 +1,309 @@
/**
* Smoke tests for additional assertions, transforms, and feature integration.
*
* Tests provider named functions, test generators, additional assertion types,
* response transforms, and advanced config features.
*
* @see docs/plans/smoke-tests.md for the full checklist
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Path to the built CLI binary
const CLI_PATH = path.resolve(__dirname, '../../dist/src/main.js');
const ROOT_DIR = path.resolve(__dirname, '../..');
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const CONFIGS_DIR = path.resolve(FIXTURES_DIR, 'configs');
const OUTPUT_DIR = path.resolve(__dirname, '.temp-output-features');
/**
* Helper to run the CLI and capture output
*/
function runCli(
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync('node', [CLI_PATH, ...args], {
cwd: options.cwd || ROOT_DIR,
encoding: 'utf-8',
env: { ...process.env, ...options.env, NO_COLOR: '1' },
timeout: 60000,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status ?? 1,
};
}
describe('Named Function Provider Smoke Tests', () => {
beforeAll(() => {
if (!fs.existsSync(CLI_PATH)) {
throw new Error(`Built CLI not found at ${CLI_PATH}. Run 'npm run build' first.`);
}
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('5.1 Additional Assertions', () => {
it('5.1.2 - not-contains assertion', () => {
const configPath = path.join(CONFIGS_DIR, 'not-contains-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'not-contains-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
});
describe('3.4 Python Provider Named Functions', () => {
it('3.4.2 - Python provider with named function', () => {
const configPath = path.join(CONFIGS_DIR, 'provider-python-named.yaml');
const outputPath = path.join(OUTPUT_DIR, 'python-named-output.json');
const { exitCode, stdout, stderr } = runCli(
['eval', '-c', configPath, '-o', outputPath, '--no-cache'],
{ cwd: CONFIGS_DIR },
);
// Python may not be available in all environments
if (exitCode !== 0) {
const output = stdout + stderr;
if (
output.toLowerCase().includes('python') &&
(output.toLowerCase().includes('not found') ||
output.toLowerCase().includes('no such file'))
) {
return;
}
}
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
expect(parsed.results.results[0].response.output).toContain('Python Custom Echo:');
});
});
});
describe('Test Generator Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('4.2 JavaScript Test Generator', () => {
it('4.2.7 - loads tests from JavaScript generator file', () => {
const configPath = path.join(CONFIGS_DIR, 'js-test-generator.yaml');
const outputPath = path.join(OUTPUT_DIR, 'js-generator-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 3 test results from the generator
expect(parsed.results.results.length).toBe(3);
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('Generated1'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Generated2'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Generated3'))).toBe(true);
});
});
});
describe('Additional Assertion Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('5.1 Built-in Assertions', () => {
it('5.1.1 - contains assertion', () => {
const configPath = path.join(CONFIGS_DIR, 'contains-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'contains-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
it('5.1.4 - starts-with assertion', () => {
const configPath = path.join(CONFIGS_DIR, 'starts-with-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'starts-with-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
it('5.1.7 - contains-json assertion', () => {
const configPath = path.join(CONFIGS_DIR, 'contains-json-assertion.yaml');
const outputPath = path.join(OUTPUT_DIR, 'contains-json-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
});
});
describe('Transform Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('6.1 Response Transforms', () => {
it('6.1.1 - transform response expression', () => {
const configPath = path.join(CONFIGS_DIR, 'transform-response.yaml');
const outputPath = path.join(OUTPUT_DIR, 'transform-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
});
});
});
describe('Feature Integration Smoke Tests', () => {
beforeAll(() => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('7.1 Provider Config Options', () => {
it('7.1.1 - provider with config options', () => {
const configPath = path.join(CONFIGS_DIR, 'provider-with-config.yaml');
const outputPath = path.join(OUTPUT_DIR, 'provider-config-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
expect(parsed.results.results[0].success).toBe(true);
// Check that the provider label is used
expect(parsed.results.prompts[0].provider).toBe('Custom Echo Provider');
});
});
describe('7.2 DefaultTest', () => {
it('7.2.1 - defaultTest applies assertions to all tests', () => {
const configPath = path.join(CONFIGS_DIR, 'default-test.yaml');
const outputPath = path.join(OUTPUT_DIR, 'default-test-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// All 3 tests should pass with the defaultTest assertion
expect(parsed.results.results.length).toBe(3);
expect(parsed.results.results.every((r: { success: boolean }) => r.success)).toBe(true);
});
});
describe('7.3 Scenarios', () => {
it('7.3.1 - scenarios feature loads and runs correctly', () => {
const configPath = path.join(CONFIGS_DIR, 'scenarios.yaml');
const outputPath = path.join(OUTPUT_DIR, 'scenarios-output.json');
const { exitCode } = runCli(['eval', '-c', configPath, '-o', outputPath, '--no-cache'], {
cwd: CONFIGS_DIR,
});
expect(exitCode).toBe(0);
const content = fs.readFileSync(outputPath, 'utf-8');
const parsed = JSON.parse(content);
// Should have 3 tests total (2 from US scenario + 1 from EU scenario)
expect(parsed.results.results.length).toBe(3);
expect(parsed.results.results.every((r: { success: boolean }) => r.success)).toBe(true);
// Verify scenarios data
const outputs = parsed.results.results.map(
(r: { response: { output: string } }) => r.response.output,
);
expect(outputs.some((o: string) => o.includes('New York'))).toBe(true);
expect(outputs.some((o: string) => o.includes('California'))).toBe(true);
expect(outputs.some((o: string) => o.includes('Paris'))).toBe(true);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
/**
* JavaScript assertion file (5.2.2)
* Checks that output meets minimum length requirement
*/
module.exports = (output, context) => {
const minLength = context.test?.assert?.[0]?.config?.minLength || 5;
const actualLength = output.length;
if (actualLength >= minLength) {
return {
pass: true,
score: 1.0,
reason: `Output length (${actualLength}) meets minimum (${minLength})`,
};
}
return {
pass: false,
score: actualLength / minLength,
reason: `Output length (${actualLength}) below minimum (${minLength})`,
};
};
@@ -0,0 +1,22 @@
"""
Python assertion file (5.2.5)
Checks that output contains expected keywords
"""
def get_assert(output, context):
"""Check if output contains the expected keyword."""
expected = context.get("test", {}).get("vars", {}).get("expected_word", "hello")
if expected.lower() in output.lower():
return {
"pass": True,
"score": 1.0,
"reason": f"Output contains '{expected}'",
}
return {
"pass": False,
"score": 0.0,
"reason": f"Output does not contain '{expected}'",
}
@@ -0,0 +1,11 @@
/**
* Dynamic assertion value script (#6253)
*
* This script returns a value to be used in an assertion.
* Tests that file:// references in assertion values use script output.
*/
module.exports = function () {
// Return a dynamic value that the assertion will use
return 'DynamicValue';
};
@@ -0,0 +1,10 @@
"""
Dynamic assertion value script (#6253)
This script returns a value to be used in an assertion.
Tests that file:// references in assertion values use script output.
"""
def get_value():
return "PythonDynamicValue"
@@ -0,0 +1,31 @@
// Assertion function that verifies dynamic vars are resolved
// Tests fix for GitHub issue #7334
module.exports = function (_output, context) {
const dynamicVar = context.vars.DYNAMIC_VAR;
// Check if the variable was resolved (should be an ISO date string)
// or still has the file:// prefix (bug)
if (typeof dynamicVar === 'string' && dynamicVar.startsWith('file://')) {
return {
pass: false,
score: 0,
reason: `BUG: DYNAMIC_VAR was not resolved! Got raw file path: ${dynamicVar}`,
};
}
// Validate it's a valid ISO date
const date = new Date(dynamicVar);
if (isNaN(date.getTime())) {
return {
pass: false,
score: 0,
reason: `DYNAMIC_VAR is not a valid date: ${dynamicVar}`,
};
}
return {
pass: true,
score: 1,
reason: `DYNAMIC_VAR was correctly resolved to: ${dynamicVar}`,
};
};
@@ -0,0 +1,50 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - various assertion types'
providers:
- echo
prompts:
- '{{input}}'
tests:
# Test equals assertion
- vars:
input: 'exact match test'
assert:
- type: equals
value: 'exact match test'
# Test regex assertion
- vars:
input: 'The answer is 42'
assert:
- type: regex
value: 'answer.*\d+'
# Test starts-with assertion
- vars:
input: 'Hello World'
assert:
- type: starts-with
value: 'Hello'
# Test is-json assertion
- vars:
input: '{"key": "value", "number": 123}'
assert:
- type: is-json
# Test not-contains assertion
- vars:
input: 'This is safe content'
assert:
- type: not-contains
value: 'dangerous'
# Test inline JavaScript assertion
- vars:
input: 'count to five: 1 2 3 4 5'
assert:
- type: javascript
value: output.includes('5') && output.length > 10
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://promptfoo.dev/config-schema.json",
"description": "Smoke test - JSON config format",
"providers": ["echo"],
"prompts": ["Hello {{name}}"],
"tests": [
{
"vars": { "name": "JSON" },
"assert": [{ "type": "contains", "value": "Hello" }, { "type": "contains", "value": "JSON" }]
}
]
}
+17
View File
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - basic config validation'
providers:
- echo
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: Hello
- type: contains
value: World
@@ -0,0 +1,23 @@
# Test config for GitHub issue #7266
# Provider returns data with circular references (simulating leaked Timeout objects)
# This should fail on main but pass with the fix
#
# NOTE: This test intentionally uses a custom provider instead of 'echo' because:
# - The echo provider cannot generate circular references in its output
# - This test must verify that circular reference objects (like Node.js Timeout)
# are properly sanitized before database storage
# - The custom provider creates a controlled circular reference structure
# (similar to _idlePrev/_idleNext in Node.js timers) to reproduce the bug
description: 'Regression test for #7266 - circular reference in provider response'
providers:
- file://../providers/circular-ref-provider.js
prompts:
- 'Test prompt'
tests:
- vars: {}
assert:
- type: contains
value: 'Processed'
@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.1.1: CJS provider with module.exports still works after ESM migration
# Bug #6501: .js files with CJS syntax failed to load in 0.120.0
description: 'Regression test - CJS module.exports provider'
providers:
- file://../providers/cjs-module-exports.js
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: CjsTest
assert:
- type: contains
value: 'CJS Echo:'
- type: contains
value: CjsTest
@@ -0,0 +1,21 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.1.2: CJS provider that uses require() internally
# Bug #6468: require() resolution was broken in 0.120.0
description: 'Regression test - CJS provider with require()'
providers:
- file://../providers/cjs-with-require.js
prompts:
- 'Test {{value}}'
tests:
- vars:
value: RequireTest
assert:
- type: contains
value: 'Require Test:'
- type: contains
value: 'platform='
- type: contains
value: RequireTest
@@ -0,0 +1,16 @@
# Test config for #7353 - class-based provider with prototype id() method
# This verifies that class-based providers work correctly through the CLI.
description: 'Test class-based provider with prototype id()'
providers:
- file://../providers/class-provider-prototype-id.js
prompts:
- 'Hello World'
tests:
- assert:
- type: contains
value: ClassProvider
- type: contains
value: Hello World
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Base config for testing multiple config merging
description: 'Base config'
providers:
- echo
prompts:
- 'Hello {{name}}'
@@ -0,0 +1,17 @@
/**
* CJS config using module.exports object (2.3.1)
*/
module.exports = {
description: 'Smoke test - CJS config format',
providers: ['echo'],
prompts: ['Hello from CJS config: {{name}}'],
tests: [
{
vars: { name: 'CommonJS' },
assert: [
{ type: 'contains', value: 'Hello from CJS' },
{ type: 'contains', value: 'CommonJS' },
],
},
],
};
@@ -0,0 +1,17 @@
/**
* ESM config using export default (2.3.5)
*/
export default {
description: 'Smoke test - ESM config format',
providers: ['echo'],
prompts: ['Hello from ESM config: {{name}}'],
tests: [
{
vars: { name: 'ESModule' },
assert: [
{ type: 'contains', value: 'Hello from ESM' },
{ type: 'contains', value: 'ESModule' },
],
},
],
};
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Complete config for testing config-based features
description: 'Smoke test - config extension'
providers:
- echo
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: ConfigMergeTest
assert:
- type: contains
value: ConfigMergeTest
+34
View File
@@ -0,0 +1,34 @@
/**
* TypeScript config using export default (2.4.1)
*
* Uses inline type to avoid depcheck issues with 'promptfoo' import.
* In real usage, you would import from 'promptfoo':
* import type { UnifiedConfig } from 'promptfoo';
*/
interface UnifiedConfig {
description?: string;
providers: string[];
prompts: string[];
tests: Array<{
vars: Record<string, string>;
assert: Array<{ type: string; value: string }>;
}>;
}
const config: UnifiedConfig = {
description: 'Smoke test - TypeScript config format',
providers: ['echo'],
prompts: ['Hello from TypeScript config: {{name}}'],
tests: [
{
vars: { name: 'TypeScript' },
assert: [
{ type: 'contains', value: 'Hello from TypeScript' },
{ type: 'contains', value: 'TypeScript' },
],
},
],
};
export default config;
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing contains-all assertion
description: 'Smoke test - contains-all assertion'
providers:
- echo
prompts:
- 'Hello World, this is a test message'
tests:
- assert:
# All values must be present
- type: contains-all
value:
- Hello
- World
- test
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing contains-any assertion
description: 'Smoke test - contains-any assertion'
providers:
- echo
prompts:
- 'The quick brown fox'
tests:
- assert:
# At least one value must be present
- type: contains-any
value:
- elephant
- fox
- giraffe
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - contains assertion (5.1.1)'
providers:
- echo
prompts:
- 'The quick brown fox jumps over the lazy dog'
tests:
- vars: {}
assert:
- type: contains
value: 'quick brown'
- type: contains
value: 'lazy dog'
@@ -0,0 +1,27 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - contains-json assertion (5.1.7)'
providers:
- echo
prompts:
# The echo provider echoes the prompt, so include JSON in the prompt
- 'Here is the data: {"status": "success", "code": 200, "message": "Hello"}'
tests:
- vars: {}
assert:
# contains-json without value just checks that valid JSON is present in output
- type: contains-json
# With a value, it validates against a JSON schema
- type: contains-json
value:
type: object
required:
- status
- code
properties:
status:
type: string
code:
type: number
@@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing quoted comma parsing across all contains assertion types
description: 'Smoke test - quoted comma parsing in contains assertions'
providers:
- echo
prompts:
- 'hello, world and foo bar'
tests:
# contains-any with quoted comma value (string format)
- assert:
- type: contains-any
value: '"hello, world",universe'
# contains-all with quoted comma values (string format)
- assert:
- type: contains-all
value: '"hello, world","foo"'
# icontains-any with quoted comma value
- assert:
- type: icontains-any
value: '"HELLO, WORLD",universe'
# icontains-all with quoted comma values
- assert:
- type: icontains-all
value: '"HELLO, WORLD","FOO"'
@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - conversation relevance SSTI'
prompts:
- 'unused prompt'
providers:
- echo
defaultTest:
options:
provider: 'exec:node ../scripts/capture-conversation-relevance-grader.js'
tests:
- vars:
country: France
capital: Paris
_conversation:
- input: 'What is the capital of {{country}}?'
output: 'The answer is {{capital}} and {{ env.OPENAI_API_KEY }} and {% for x in range(3) %}{{x}}{% endfor %}'
assert:
- type: conversation-relevance
threshold: 0
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing cost assertion
description: 'Smoke test - cost assertion'
providers:
- echo
prompts:
- 'Test prompt for cost check'
tests:
- assert:
# Echo provider has 0 cost, should pass with any threshold
- type: cost
threshold: 1.0
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - CSV test loading'
providers:
- echo
prompts:
- 'Hello {{name}}, you are {{age}} years old'
tests: file://tests.csv
@@ -0,0 +1,22 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - defaultTest feature (7.2.1)'
providers:
- echo
prompts:
- 'Hello {{name}}'
# Default assertions applied to all tests
defaultTest:
assert:
- type: contains
value: 'Hello'
tests:
- vars:
name: Alice
- vars:
name: Bob
- vars:
name: Charlie
@@ -0,0 +1,7 @@
options:
provider:
id: file://../providers/defaulttest-llm-rubric-grader.cjs
assert:
- type: llm-rubric
value: 'Does the output correctly reference the input: {{myVar}}?'
@@ -0,0 +1,4 @@
- vars:
myVar: 'hello world'
- vars:
myVar: 'goodbye moon'
@@ -0,0 +1,13 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Regression test - defaultTest llm-rubric vars'
prompts:
- 'static model output'
providers:
- echo
defaultTest: file://defaulttest-llm-rubric-vars-default.yaml
tests:
- file://defaulttest-llm-rubric-vars-tests.yaml
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Simple config for testing --delay flag
description: 'Smoke test - delay between tests'
providers:
- echo
prompts:
- 'Test {{num}}'
tests:
- vars:
num: '1'
- vars:
num: '2'
- vars:
num: '3'
@@ -0,0 +1,23 @@
# Regression test for GitHub Issue #7334
# Dynamic variables should be resolved when passed to assertion functions
# https://github.com/promptfoo/promptfoo/issues/7334
providers:
- echo
prompts:
- 'The dynamic variable is: {{DYNAMIC_VAR}}'
defaultTest:
vars:
# This dynamic variable is loaded from a JavaScript file
DYNAMIC_VAR: file://dynamic-var-generator.js
tests:
- description: 'Dynamic vars should be resolved in assertion context.vars'
assert:
# This assertion function accesses context.vars.DYNAMIC_VAR
# Before fix: received "file://dynamic-var-generator.js"
# After fix: receives the resolved ISO date string
- type: javascript
value: file://assert-dynamic-var.js
@@ -0,0 +1,7 @@
// Dynamic variable that returns current ISO UTC timestamp
// Used to test that file:// vars are resolved before being passed to assertions
module.exports = function (_varName, _prompt, _vars, _provider) {
return {
output: new Date().toISOString(),
};
};
@@ -0,0 +1,10 @@
description: 'Test empty vars handling'
providers:
- echo
prompts:
- 'Static prompt'
tests:
- vars: {}
assert:
- type: contains
value: Static
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing regex assertion with end-of-string pattern
description: 'Smoke test - regex ends-with pattern'
providers:
- echo
prompts:
- 'The answer is 42.'
tests:
- assert:
# Regex pattern to match string ending with "42."
- type: regex
value: '42\.$'
# Regex pattern to match string ending with "."
- type: regex
value: '\.$'
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing --env-file flag
description: 'Smoke test - environment variable loading'
providers:
- echo
prompts:
- 'API Key: {{api_key}}, Secret: {{secret}}'
tests:
- vars:
api_key: '{{env.TEST_API_KEY}}'
secret: '{{env.TEST_SECRET}}'
assert:
- type: contains
value: 'API Key:'
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - exec provider closes stdin (regression for #8686)'
providers:
- 'exec:node ../providers/exec-provider-reads-stdin.js'
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: stdin closed
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - exec provider'
providers:
- 'exec:echo "Echo from exec: {{prompt}}"'
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: Echo from exec
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - config with failing assertion'
providers:
- echo
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
# This assertion will fail because echo returns "Hello World"
# but we're asserting it contains "IMPOSSIBLE_STRING_NOT_IN_OUTPUT"
- type: contains
value: IMPOSSIBLE_STRING_NOT_IN_OUTPUT_12345
@@ -0,0 +1,38 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config with some intentionally failing tests for --filter-failing testing
description: 'Smoke test - config with failing tests'
providers:
- echo
prompts:
- 'Hello {{name}}'
tests:
- description: 'passing test 1'
vars:
name: Alice
assert:
- type: contains
value: Alice
- description: 'failing test 1'
vars:
name: Bob
assert:
- type: contains
value: 'WILL_NOT_MATCH_12345'
- description: 'passing test 2'
vars:
name: Charlie
assert:
- type: contains
value: Charlie
- description: 'failing test 2'
vars:
name: Diana
assert:
- type: contains
value: 'ALSO_NOT_MATCHING_67890'
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - file:// prompt loading'
providers:
- echo
prompts:
- file://prompt-template.txt
tests:
- vars:
topic: bananas
assert:
- type: contains
value: bananas
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - file provider env placeholders'
providers:
- file://../providers/file-provider-env-7079.yaml
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: World
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test for #6253: file:// references in assertion values use script output
description: 'Regression test - file:// references in assertion values'
providers:
- echo
prompts:
- 'Test DynamicValue here'
tests:
- assert:
# The file:// reference should execute the script and use its return value
- type: contains
value: file://../assertions/dynamic-value.js
@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test for #6393: file:// references in vars context for runtime loading
description: 'Regression test - file:// references in vars'
providers:
- echo
prompts:
- 'Value is {{dynamicVar}}'
# Load tests from a file that includes vars
tests: file://../data/tests-with-vars.yaml
@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test for #6174: function providers in defaultTest.options.provider
description: 'Regression test - function providers in defaultTest'
providers:
- echo
prompts:
- 'Test {{name}}'
# Function provider in defaultTest.options.provider should work
defaultTest:
assert:
- type: contains
value: Test
tests:
- vars:
name: FunctionProvider
@@ -0,0 +1,35 @@
/**
* Regression fixture for #9383: multiple CallApiFunction providers must each
* survive combineConfigs (the JSON.stringify-based dedupe used to collapse them
* into a single entry).
*
* Mirrors the repro from the issue: three labeled function providers that each
* embed their label in the response so the eval output is distinguishable.
*/
interface CallApiFunction {
(prompt: string): Promise<{ output: string }>;
label?: string;
}
interface UnifiedConfig {
description?: string;
providers: CallApiFunction[];
prompts: string[];
tests: Array<{ vars: Record<string, string> }>;
}
const makeProvider = (label: string): CallApiFunction => {
const fn: CallApiFunction = async (prompt) => ({ output: `${label}: ${prompt}` });
fn.label = label;
return fn;
};
const config: UnifiedConfig = {
description: 'Smoke test - multiple function providers (#9383)',
providers: [makeProvider('a'), makeProvider('b'), makeProvider('c')],
prompts: ['{{prompt}}'],
tests: [{ vars: { prompt: 'hi' } }],
};
export default config;
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing multiple prompts from files
description: 'Smoke test - multiple file prompts'
providers:
- echo
prompts:
- file://../prompts/greeting.txt
- file://../prompts/farewell.txt
tests:
- vars:
name: GlobTest
assert:
- type: contains
value: GlobTest
@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.5.1: Go provider works after ESM migration
# Bug #6506: Go provider wrapper failed in 0.120.0
description: 'Regression test - Go provider'
providers:
- file://../providers/echo-go.go
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: GoTest
assert:
- type: contains
value: 'Go Echo:'
- type: contains
value: GoTest
@@ -0,0 +1,21 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing icontains (case-insensitive contains) assertion
description: 'Smoke test - icontains assertion'
providers:
- echo
prompts:
- 'Response: HELLO World Test'
tests:
- assert:
# Case-insensitive match - "hello" matches "HELLO"
- type: icontains
value: hello
# Case-insensitive match - "WORLD" matches "World"
- type: icontains
value: WORLD
# Case-insensitive match - mixed case
- type: icontains
value: wOrLd
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing inline JavaScript assertion
description: 'Smoke test - inline JavaScript assertion'
providers:
- echo
prompts:
- 'The answer is 42'
tests:
- assert:
# Inline JavaScript expression
- type: javascript
value: 'output.includes("42") && output.length > 10'
@@ -0,0 +1,27 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.1.3: Inline JS with process.mainModule shim
# Bug #6606: Inline transforms using process.mainModule.require broke in 0.120.0
#
# After ESM migration, process.mainModule is undefined. The fix adds a shim
# that provides process.mainModule.require via createRequire().
description: 'Regression test - inline JS with process access'
providers:
- echo
prompts:
- 'Test {{value}}'
tests:
- vars:
value: ProcessTest
assert:
- type: javascript
value: |
// Test that process object is accessible in inline JS
// This broke in 0.120.0 because process.mainModule was undefined
// Note: 'output' is already defined in the context, use it directly
const hasProcess = typeof process !== 'undefined';
const hasEnv = hasProcess && typeof process.env !== 'undefined';
// The assertion should pass if process is accessible
return output.includes('ProcessTest') && hasProcess && hasEnv;
+14
View File
@@ -0,0 +1,14 @@
# This config is intentionally invalid for testing validation errors
description: 'Invalid config for smoke testing'
# Missing required 'providers' field
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: invalid-assertion-type-that-does-not-exist
value: test
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - JavaScript file assertion'
providers:
- echo
prompts:
- 'This is a test message with some content: {{topic}}'
tests:
- vars:
topic: bananas
assert:
- type: javascript
value: file://../assertions/check-length.js
config:
minLength: 10
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - JavaScript test generator (4.2.7)'
providers:
- echo
prompts:
- 'Hello {{name}}'
tests: file://../data/test-generator.js
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 11.2.1: JSON chat message parsing
# Bug #6568: Incorrect parsing of JSON vs non-JSON chat messages in 0.120.1
description: 'Regression test - JSON chat message parsing'
providers:
- echo
# This uses a JSON file containing chat messages in array format
prompts:
- file://../prompts/chat.json
tests:
- vars:
name: ChatParseTest
assert:
- type: contains
value: ChatParseTest
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing JSON chat format prompts
description: 'Smoke test - JSON chat format prompt'
providers:
- echo
prompts:
- file://../prompts/chat.json
tests:
- vars:
name: ChatTest
assert:
- type: contains
value: ChatTest
@@ -0,0 +1,25 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing contains-json with JSON Schema validation
description: 'Smoke test - JSON schema validation via contains-json'
providers:
- echo
prompts:
- '{"name": "John", "age": 30}'
tests:
- assert:
- type: is-json
# contains-json with a value validates against JSON Schema
- type: contains-json
value:
type: object
required:
- name
- age
properties:
name:
type: string
age:
type: number
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - JSON test loading'
providers:
- echo
prompts:
- 'Greet {{name}}'
tests: file://tests.json
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - JSONL test loading'
providers:
- echo
prompts:
- 'Greet {{name}}'
tests: file://tests.jsonl
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing latency assertion
description: 'Smoke test - latency assertion'
providers:
- echo
prompts:
- 'Test prompt for latency check'
tests:
- assert:
# Echo provider is fast, should complete under 5000ms
- type: latency
threshold: 5000
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing levenshtein (edit distance) assertion
description: 'Smoke test - levenshtein assertion'
providers:
- echo
prompts:
- 'Hello World'
tests:
- assert:
# Allow up to 3 character edits
- type: levenshtein
value: 'Hello Wurld'
threshold: 3
@@ -0,0 +1,73 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: Repro for stateful Ollama-style judge model switches inside assert-set
prompts:
- 'Answer this prompt: {{topic}}'
providers:
- echo
tests:
- vars:
topic: alpha
assert:
- type: assert-set
assert:
- type: llm-rubric
value: Judge alpha with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge alpha with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- vars:
topic: beta
assert:
- type: assert-set
assert:
- type: llm-rubric
value: Judge beta with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge beta with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- vars:
topic: gamma
assert:
- type: assert-set
assert:
- type: llm-rubric
value: Judge gamma with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge gamma with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
@@ -0,0 +1,67 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: Repro for stateful Ollama-style judge model switches
prompts:
- 'Answer this prompt: {{topic}}'
providers:
- echo
tests:
- vars:
topic: alpha
assert:
- type: llm-rubric
value: Judge alpha with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge alpha with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- vars:
topic: beta
assert:
- type: llm-rubric
value: Judge beta with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge beta with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- vars:
topic: gamma
assert:
- type: llm-rubric
value: Judge gamma with model one
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-one
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
- type: llm-rubric
value: Judge gamma with model two
provider:
id: file://../providers/stateful-ollama-grader.cjs
config:
modelName: judge-two
logPath: '{{ env.PROMPTFOO_STATEFUL_GRADER_LOG_PATH | default("") }}'
reloadDelayMs: '{{ env.PROMPTFOO_STATEFUL_GRADER_RELOAD_DELAY_MS | default(0) }}'
@@ -0,0 +1,32 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.3.2: maxConcurrency in config.yaml is respected
# Bug #6526: maxConcurrency from config file was ignored in 0.120.0
description: 'Regression test - maxConcurrency from config file'
providers:
- echo
prompts:
- 'Test {{n}}'
# This setting was ignored in 0.120.0 - only CLI --max-concurrency worked
defaultTest:
options:
maxConcurrency: 1
tests:
- vars:
n: '1'
assert:
- type: contains
value: '1'
- vars:
n: '2'
assert:
- type: contains
value: '2'
- vars:
n: '3'
assert:
- type: contains
value: '3'
@@ -0,0 +1,15 @@
description: 'Test multiple assertion types'
providers:
- echo
prompts:
- 'Hello World 123'
tests:
- assert:
- type: contains
value: Hello
- type: contains
value: World
- type: regex
value: '\d+'
- type: javascript
value: output.length > 5
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing multiple prompts
description: 'Smoke test - multiple prompts comparison'
providers:
- echo
prompts:
- 'Greeting: Hello {{name}}!'
- 'Greeting: Hi {{name}}!'
- 'Greeting: Hey {{name}}!'
tests:
- vars:
name: Alice
assert:
- type: contains
value: Alice
@@ -0,0 +1,21 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config with multiple providers for provider filtering
description: 'Smoke test - multiple providers for filtering'
providers:
- id: echo
label: 'Echo Provider'
- id: echo
label: 'Custom Echo'
- id: echo
label: 'Test Provider'
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: World
@@ -0,0 +1,68 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config with multiple tests for filter testing
description: 'Smoke test - multiple tests for filtering'
providers:
- echo
prompts:
- 'Hello {{name}}, you are a {{role}}'
tests:
- description: 'user authentication test'
vars:
name: Alice
role: admin
metadata:
category: auth
priority: high
assert:
- type: contains
value: Alice
- description: 'user profile test'
vars:
name: Bob
role: user
metadata:
category: profile
priority: medium
assert:
- type: contains
value: Bob
- description: 'admin dashboard test'
vars:
name: Charlie
role: admin
metadata:
category: admin
priority: high
assert:
- type: contains
value: Charlie
- description: 'guest access test'
vars:
name: Diana
role: guest
metadata:
category: auth
priority: low
assert:
- type: contains
value: Diana
- description: 'settings update test'
vars:
name: Eve
role: user
metadata:
category: settings
priority: medium
tags:
- security
- config
assert:
- type: contains
value: Eve
@@ -0,0 +1,62 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - OpenAI tool format with cross-provider transformation'
providers:
# Test OpenAI tools with auto mode
- id: 'exec:node ../scripts/echo-config.js'
label: 'Auto tool choice'
config:
tools: &tools
- type: function
function:
name: get_weather
description: Get current weather for a location
parameters:
type: object
properties:
location:
type: string
description: City name
unit:
type: string
enum: [celsius, fahrenheit]
required:
- location
- type: function
function:
name: search_web
description: Search the web for information
parameters:
type: object
properties:
query:
type: string
required:
- query
tool_choice: auto
# Test tool reuse with YAML alias and required mode
- id: 'exec:node ../scripts/echo-config.js'
label: 'Required tool choice'
config:
tools: *tools
tool_choice: required
# Test specific tool choice
- id: 'exec:node ../scripts/echo-config.js'
label: 'Specific tool choice'
config:
tools: *tools
tool_choice:
type: function
function:
name: get_weather
prompts:
- 'What is the weather in {{location}}?'
tests:
- vars:
location: San Francisco
assert:
- type: is-json
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - not-contains assertion (5.1.2)'
providers:
- echo
prompts:
- 'The quick brown fox jumps over the lazy dog'
tests:
- vars: {}
assert:
- type: not-contains
value: 'elephant'
- type: not-contains
value: 'giraffe'
@@ -0,0 +1,49 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - negated script assertions'
providers:
- echo
prompts:
- '{{value}}'
tests:
- description: not-javascript inverts a boolean return
vars:
value: safe-output
assert:
- type: not-javascript
value: output.includes('forbidden')
- description: not-python preserves score when a numeric return is below threshold
vars:
value: safe-output
assert:
- type: not-python
value: '0.25'
threshold: 0.5
- description: not-python inverts a JSON-stringified low-score GradingResult
vars:
value: safe-output
assert:
- type: not-python
value: |
return '{"pass": true, "score": 0.25, "reason": "Python raw low score"}'
threshold: 0.5
- description: not-ruby inverts an object return
vars:
value: safe-output
assert:
- type: not-ruby
value: "{ pass_: false, score: 0.4, reason: 'Ruby raw false' }"
- description: not-ruby inverts a JSON-stringified low-score GradingResult
vars:
value: safe-output
assert:
- type: not-ruby
value: |
return '{"pass": true, "score": 0.4, "reason": "Ruby raw low score"}'
threshold: 0.5
@@ -0,0 +1,16 @@
description: 'Test Nunjucks conditionals'
providers:
- echo
prompts:
- '{% if premium %}Premium user{% else %}Free user{% endif %}'
tests:
- vars:
premium: true
assert:
- type: contains
value: Premium
- vars:
premium: false
assert:
- type: contains
value: Free
@@ -0,0 +1,11 @@
description: 'Test Nunjucks filters'
providers:
- echo
prompts:
- 'Hello {{ name | upper }}'
tests:
- vars:
name: world
assert:
- type: contains
value: WORLD
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - OpenAI assistant custom apiKeyEnvar missing key error'
providers:
- id: openai:assistant:test-assistant-id
label: 'OpenAI assistant with custom key'
config:
# Keep evaluation running so the missing-key message is captured in the output JSON.
apiKeyRequired: false
apiKeyEnvar: CUSTOM_ASSISTANT_KEY
prompts:
- 'Hello from smoke test'
tests:
- vars: {}
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing prompt prefix/suffix
description: 'Smoke test - prompt prefix and suffix'
providers:
- echo
prompts:
- 'MIDDLE'
tests:
- vars: {}
assert:
- type: contains
value: MIDDLE
@@ -0,0 +1 @@
Tell me about {{topic}} in one sentence.
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - CJS provider class'
providers:
- file://../providers/echo-cjs.cjs
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: CJS
assert:
- type: contains
value: 'CJS Echo:'
- type: contains
value: CJS
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - ESM provider class'
providers:
- file://../providers/echo-esm.mjs
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: ESM
assert:
- type: contains
value: 'ESM Echo:'
- type: contains
value: ESM
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config for testing provider label in output
description: 'Smoke test - provider label'
providers:
- id: echo
label: 'My Custom Echo Provider'
prompts:
- 'Test prompt'
tests:
- assert:
- type: contains
value: 'Test'
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - Python provider with named function (3.4.2)'
providers:
- file://../providers/echo_provider_named.py:custom_echo
prompts:
- 'Test prompt: {{input}}'
tests:
- vars:
input: hello python
assert:
- type: contains
value: 'Python Custom Echo:'
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - TypeScript provider with transitive helper import'
providers:
- file://../providers/echo-ts-transitive.ts
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: helper
assert:
- type: contains
value: 'TypeScript Transitive Echo:'
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - TypeScript provider class'
providers:
- file://../providers/echo-ts.ts
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: TypeScript
assert:
- type: contains
value: 'TypeScript Echo:'
- type: contains
value: TypeScript
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - provider with config options (7.1.1)'
providers:
- id: echo
label: 'Custom Echo Provider'
config:
customOption: 'test-value'
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: ConfigTest
assert:
- type: contains
value: 'ConfigTest'
@@ -0,0 +1,35 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config with multiple providers that have labels and config options.
# Uses ProviderOptionsMap format with overridden IDs so that the bare
# string 'echo' does NOT match any config provider by id/label/suffix,
# enabling a true fallback-to-bare-token smoke test.
description: 'Smoke test - providers flag preserves config'
providers:
- echo:
id: 'alpha-echo'
label: 'provider-alpha'
config:
temperature: 0.1
custom_option: 'alpha-value'
- echo:
id: 'beta-echo'
label: 'provider-beta'
config:
temperature: 0.9
custom_option: 'beta-value'
- echo:
id: 'gamma-echo'
label: 'provider-gamma'
config:
temperature: 0.5
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: World
assert:
- type: contains
value: World
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - Python file assertion'
providers:
- echo
prompts:
- 'Hello {{name}}, welcome to the system'
tests:
- vars:
name: Alice
expected_word: Alice
assert:
- type: python
value: file://../assertions/check_keywords.py
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - Python prompt function with Python extension hook (issue #9653)'
extensions:
- file://../scripts/extension_hook.py:extension_hook
prompts:
- id: file://../prompts/create_prompt.py:create_prompt
label: hooked-prompt
providers:
- echo
tests:
- vars:
topic: Linear Algebra
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - Python provider'
providers:
- file://../providers/echo_provider.py
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: Python
assert:
- type: contains
value: 'Python Echo:'
- type: contains
value: Python
@@ -0,0 +1,30 @@
{
"$schema": "https://promptfoo.dev/config-schema.json",
"commandLineOptions": {
"filterRange": "1:3"
},
"prompts": ["Hello {{name}}"],
"providers": ["echo"],
"tests": [
{
"vars": {
"name": "Alice"
}
},
{
"vars": {
"name": "Bob"
}
},
{
"vars": {
"name": "Charlie"
}
},
{
"vars": {
"name": "David"
}
}
]
}
@@ -0,0 +1,25 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test config for #7353 - class-based provider with prototype id() method in redteam flow
# This verifies that class-based providers work correctly in the redteam generate flow.
#
# The bug was: wrapProviderWithRateLimiting used spread operator which doesn't copy
# prototype methods like id(), causing "TypeError: redteamProvider.id is not a function"
# in strategies that call TokenUsageTracker.trackUsage(provider.id(), ...).
#
# Uses contracts plugin (local generation) and base64 strategy (local transform)
# so the test completes without any remote API calls.
description: 'Test class-based redteam provider with prototype id()'
targets:
- id: echo
label: echo-target
redteam:
# Use our class-based provider as the redteam/attacker provider
provider: file://../providers/redteam-class-provider-7353.js
purpose: Test application for security vulnerabilities
numTests: 1
plugins:
- contracts
strategies:
- id: base64
@@ -0,0 +1,52 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Config with many tests for resume E2E testing
# Uses delay + echo provider so we can reliably interrupt and resume
description: 'E2E test - resume with many test cases'
providers:
- echo
prompts:
- 'Hello {{name}} - item {{num}}'
tests:
- vars: { name: Alice, num: '1' }
assert:
- type: contains
value: Alice
- vars: { name: Bob, num: '2' }
assert:
- type: contains
value: Bob
- vars: { name: Charlie, num: '3' }
assert:
- type: contains
value: Charlie
- vars: { name: Diana, num: '4' }
assert:
- type: contains
value: Diana
- vars: { name: Eve, num: '5' }
assert:
- type: contains
value: Eve
- vars: { name: Frank, num: '6' }
assert:
- type: contains
value: Frank
- vars: { name: Grace, num: '7' }
assert:
- type: contains
value: Grace
- vars: { name: Heidi, num: '8' }
assert:
- type: contains
value: Heidi
- vars: { name: Ivan, num: '9' }
assert:
- type: contains
value: Ivan
- vars: { name: Judy, num: '10' }
assert:
- type: contains
value: Judy
@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Test 10.5.2: Ruby provider works after ESM migration
# Bug #6506: Ruby provider wrapper failed in 0.120.0
description: 'Regression test - Ruby provider'
providers:
- file://../providers/echo-ruby.rb
prompts:
- 'Hello {{name}}'
tests:
- vars:
name: RubyTest
assert:
- type: contains
value: 'Ruby Echo:'
- type: contains
value: RubyTest
@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: 'Smoke test - scenarios feature (7.3.1)'
providers:
- echo
prompts:
- 'Hello {{name}}, welcome to {{location}}'
scenarios:
- description: 'US users scenario'
config:
# Config must be an array of var combinations
- vars:
region: 'US'
tests:
- vars:
name: Alice
location: New York
assert:
- type: contains
value: 'New York'
- vars:
name: Bob
location: California
assert:
- type: contains
value: 'California'
- description: 'EU users scenario'
config:
- vars:
region: 'EU'
tests:
- vars:
name: Claude
location: Paris
assert:
- type: contains
value: 'Paris'
@@ -0,0 +1,33 @@
# Fixture for #7096 regression test - JSON Schema validation
# Tests that options and metadata fields work with combined/custom properties
prompts:
- 'Answer: {{question}}'
providers:
- echo
# Test combined options from multiple merged schemas
defaultTest:
options:
# From PromptConfigSchema
prefix: 'You are helpful.'
suffix: 'Be concise.'
# From OutputConfigSchema
transform: 'output.trim()'
# From GradingConfigSchema
provider: openai:gpt-4o-mini
# Additional properties
disableVarExpansion: false
tests:
- vars:
question: 'What is 2+2?'
# Test metadata with custom keys alongside internal properties
metadata:
customTag: 'math-test'
experimentId: 12345
pluginConfig:
someOption: true
assert:
- type: contains
value: '2+2'

Some files were not shown because too many files have changed in this diff Show More