Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

1191 lines
37 KiB
TypeScript

import fs from 'fs';
import os from 'os';
import path from 'path';
import * as yaml from 'js-yaml';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import * as evaluatorModule from '../../../src/evaluator';
import logger from '../../../src/logger';
import Eval from '../../../src/models/eval';
import { doEval } from '../../../src/node/doEval';
import type { Command } from 'commander';
import type { CommandLineOptions, EvaluateOptions, TestSuite } from '../../../src/types/index';
vi.mock('../../../src/evaluator', async (importOriginal) => {
return {
...(await importOriginal()),
evaluate: vi.fn().mockResolvedValue({ results: [], summary: {} }),
};
});
vi.mock('../../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
getLogLevel: vi.fn().mockReturnValue('info'),
isDebugEnabled: vi.fn().mockReturnValue(false),
}));
vi.mock('../../../src/migrate', async (importOriginal) => {
return {
...(await importOriginal()),
runDbMigrations: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock('../../../src/telemetry', () => ({
default: {
record: vi.fn(),
recordAndSendOnce: vi.fn(),
send: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock('../../../src/share', () => ({
isSharingEnabled: vi.fn().mockReturnValue(false),
createShareableUrl: vi.fn().mockResolvedValue(null),
}));
const evaluateMock = vi.mocked(evaluatorModule.evaluate);
function makeConfig(configPath: string, evaluateOptions: Partial<EvaluateOptions> = {}) {
fs.writeFileSync(
configPath,
yaml.dump({
evaluateOptions: {
// Define default values for the tests to use if none are provided.
maxConcurrency: evaluateOptions.maxConcurrency ?? 9,
repeat: evaluateOptions.repeat ?? 99,
delay: evaluateOptions.delay ?? 999,
showProgressBar: evaluateOptions.showProgressBar ?? false,
cache: evaluateOptions.cache ?? false,
timeoutMs: evaluateOptions.timeoutMs ?? 9999,
maxEvalTimeMs: evaluateOptions.maxEvalTimeMs ?? 99999,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['test prompt'],
tests: [{ vars: { input: 'test input' } }],
}),
);
}
function writeTempConfig(tmpDir: string, fileName: string, config: Record<string, unknown>) {
const configPath = path.join(tmpDir, fileName);
fs.writeFileSync(configPath, yaml.dump(config));
return configPath;
}
async function runEvalAndGetOptions(
cmdObj: Partial<CommandLineOptions & Command>,
): Promise<EvaluateOptions> {
await doEval(
{
table: false,
write: false,
...cmdObj,
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
return evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
}
describe('evaluateOptions behavior', () => {
let tmpDir: string;
let configPath: string;
let noDelayConfigPath: string;
let noRepeatConfigPath: string;
const originalExit = process.exit;
beforeAll(() => {
process.exit = vi.fn() as any;
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-test-'));
// Build a config file for the tests to use.
configPath = path.join(tmpDir, 'promptfooconfig.yaml');
makeConfig(configPath);
// Build a config that has no delay set; a delay set to >0 will override the maxConcurrency value.
noDelayConfigPath = path.join(tmpDir, 'promptfooconfig-noDelay.yaml');
makeConfig(noDelayConfigPath, { delay: 0 });
// Build a config that has no repeat set; a repeat set to >1 will override the cache value
noRepeatConfigPath = path.join(tmpDir, 'promptfooconfig-noRepeat.yaml');
makeConfig(noRepeatConfigPath, { repeat: 1 });
});
beforeEach(() => {
vi.clearAllMocks();
});
afterAll(() => {
process.exit = originalExit;
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('Reading values from config file', () => {
it('should read evaluateOptions.maxConcurrency', async () => {
const options = await runEvalAndGetOptions({
config: [noDelayConfigPath],
});
expect(options.maxConcurrency).toBe(9);
});
it('should read evaluateOptions.repeat', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.repeat).toBe(99);
});
it('should read evaluateOptions.delay', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.delay).toBe(999);
});
it('should read evaluateOptions.showProgressBar', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.showProgressBar).toBe(false);
});
it('should read evaluateOptions.cache', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.cache).toBe(false);
});
it('should read evaluateOptions.timeoutMs', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.timeoutMs).toBe(9999);
});
it('should read evaluateOptions.maxEvalTimeMs', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
});
expect(options.maxEvalTimeMs).toBe(99999);
});
});
describe('Prioritization of CLI options over config file options', () => {
it('should prioritize maxConcurrency from command line options over config file options', async () => {
const options = await runEvalAndGetOptions({
config: [noDelayConfigPath],
maxConcurrency: 5,
});
expect(options.maxConcurrency).toBe(5);
});
it('should prioritize repeat from command line options over config file options', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
repeat: 5,
});
expect(options.repeat).toBe(5);
});
it('should prioritize delay from command line options over config file options', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
delay: 5,
});
expect(options.delay).toBe(5);
});
it('should prioritize showProgressBar from command line options over config file options', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
progressBar: true,
});
expect(options.showProgressBar).toBe(true);
});
it('should prioritize cache from command line options over config file options', async () => {
const options = await runEvalAndGetOptions({
config: [noRepeatConfigPath],
cache: true,
});
expect(options.cache).toBe(true);
});
});
it('should correctly merge evaluateOptions from multiple sources', () => {
const config = {
evaluateOptions: {
maxConcurrency: 3,
showProgressBar: false,
},
providers: [],
prompts: [],
};
const initialOptions: EvaluateOptions = {
showProgressBar: true,
};
const mergedOptions = config.evaluateOptions
? { ...initialOptions, ...config.evaluateOptions }
: initialOptions;
expect(mergedOptions.maxConcurrency).toBe(3);
expect(mergedOptions.showProgressBar).toBe(false);
});
describe('Edge cases and interactions', () => {
it('should handle delay >0 forcing concurrency to 1 even with CLI override', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
maxConcurrency: 10, // This should be overridden to 1 due to delay
});
expect(options.maxConcurrency).toBe(1); // Should be forced to 1 due to delay
});
it('should handle repeat >1 with cache value passed correctly', async () => {
const options = await runEvalAndGetOptions({
config: [configPath],
cache: true,
repeat: 3,
});
expect(options.cache).toBe(true); // Cache value is passed as-is
expect(options.repeat).toBe(3); // Repeat value is correctly set
// Repeat no longer disables cache globally. Each repeat index uses its own cache namespace.
});
it('should handle undefined/null evaluateOptions gracefully', async () => {
const tempConfig = writeTempConfig(tmpDir, 'temp-test-config.yaml', {
evaluateOptions: null, // Explicitly null
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['test'],
tests: [{ vars: { input: 'test' } }],
});
await runEvalAndGetOptions({
config: [tempConfig],
});
});
it('should handle mixed CLI and config values correctly', async () => {
const options = await runEvalAndGetOptions({
config: [configPath], // Has delay: 999, maxConcurrency: 9, etc.
delay: 0, // CLI override
repeat: 1, // CLI override
// maxConcurrency not set, should use config value
});
expect(options.delay).toBeUndefined(); // CLI delay 0 should result in undefined
expect(options.repeat).toBe(1); // CLI override
expect(options.maxConcurrency).toBe(9); // From config
});
});
describe('commandLineOptions behavior', () => {
it('should respect commandLineOptions.maxConcurrency from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-maxconcurrency.yaml', {
commandLineOptions: {
maxConcurrency: 10,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.maxConcurrency).toBe(10);
});
it('should prioritize CLI --max-concurrency over commandLineOptions.maxConcurrency', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-maxconcurrency-override.yaml', {
commandLineOptions: {
maxConcurrency: 10,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
maxConcurrency: 20, // CLI override
});
expect(options.maxConcurrency).toBe(20);
});
it('should prioritize commandLineOptions.maxConcurrency over evaluateOptions.maxConcurrency', async () => {
const tempConfig = writeTempConfig(
tmpDir,
'test-commandline-vs-evaluateoptions-maxconcurrency.yaml',
{
commandLineOptions: {
maxConcurrency: 10,
},
evaluateOptions: {
maxConcurrency: 5,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
},
);
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.maxConcurrency).toBe(10); // commandLineOptions wins
});
it('should respect commandLineOptions.repeat from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-repeat.yaml', {
commandLineOptions: {
repeat: 5,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.repeat).toBe(5);
});
it('should prioritize CLI --repeat over commandLineOptions.repeat', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-repeat-override.yaml', {
commandLineOptions: {
repeat: 5,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
repeat: 10, // CLI override
});
expect(options.repeat).toBe(10);
});
it('should respect commandLineOptions.delay from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-delay.yaml', {
commandLineOptions: {
delay: 500,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.delay).toBe(500);
});
it('should prioritize CLI --delay over commandLineOptions.delay', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-delay-override.yaml', {
commandLineOptions: {
delay: 500,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
delay: 1000, // CLI override
});
expect(options.delay).toBe(1000);
});
it('should respect commandLineOptions.cache from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-cache.yaml', {
commandLineOptions: {
cache: false,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.cache).toBe(false);
});
it('should prioritize CLI --cache over commandLineOptions.cache', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-cache-override.yaml', {
commandLineOptions: {
cache: false,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
cache: true, // CLI override
});
expect(options.cache).toBe(true);
});
it('should respect commandLineOptions.generateSuggestions from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-generate-suggestions.yaml', {
commandLineOptions: {
generateSuggestions: true,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.generateSuggestions).toBe(true);
});
it('should preserve evaluateOptions.suggestionsCount from config defaults', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-suggestions-count.yaml', {
evaluateOptions: {
generateSuggestions: true,
suggestionsCount: 2,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(2);
});
it('should respect commandLineOptions.suggestionsCount from config defaults', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-commandline-suggestions-count.yaml', {
commandLineOptions: {
generateSuggestions: true,
suggestionsCount: 3,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(3);
});
it('should let commandLineOptions.suggestionsCount override evaluateOptions defaults', async () => {
const tempConfig = writeTempConfig(
tmpDir,
'test-commandline-suggestions-count-overrides-evaluate-default.yaml',
{
evaluateOptions: {
generateSuggestions: true,
},
commandLineOptions: {
suggestionsCount: 4,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
},
);
const options = await runEvalAndGetOptions({
config: [tempConfig],
});
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(4);
});
it('should persist CLI filterRange without applying it twice for regular test suites', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-range.yaml', {
providers: ['echo'],
prompts: ['Hello {{input}}'],
tests: [
{ vars: { input: 'one' } },
{ vars: { input: 'two' } },
{ vars: { input: 'three' } },
],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterRange: '1:2',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(evalRecord.runtimeOptions?.filterRange).toBe('1:2');
expect(options.filterRange).toBeUndefined();
});
it.each([
['--filter-targets', 'filterTargets'],
['--filter-providers', 'filterProviders'],
] as const)('should persist and restore %s for resumed evaluations', async (_flag, key) => {
const tempConfig = writeTempConfig(tmpDir, `test-${key}.yaml`, {
providers: [
{ id: 'echo', label: 'excluded-target' },
{ id: 'echo', label: 'selected-target' },
],
prompts: ['Hello'],
tests: [{ vars: {} }],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
[key]: 'selected-target',
},
{},
undefined,
{},
);
const initialSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
const initialEval = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
expect(initialSuite.providers.map((provider) => provider.label)).toEqual(['selected-target']);
expect(initialEval.runtimeOptions?.providerFilter).toBe('selected-target');
const resumeEval = new Eval(initialEval.config, {
id: `eval-resume-${key}`,
persisted: true,
runtimeOptions: initialEval.runtimeOptions,
});
const findByIdSpy = vi.spyOn(Eval, 'findById').mockResolvedValue(resumeEval);
evaluateMock.mockClear();
try {
await doEval(
{
table: false,
resume: resumeEval.id,
} as any,
{},
undefined,
{},
);
const resumedSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(resumedSuite.providers.map((provider) => provider.label)).toEqual([
'selected-target',
]);
} finally {
findByIdSpy.mockRestore();
}
});
it('should not treat evaluateOptions.providerFilter as a provider selection', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-ignored-provider-filter.yaml', {
evaluateOptions: {
providerFilter: 'selected-target',
},
providers: [
{ id: 'echo', label: 'excluded-target' },
{ id: 'echo', label: 'selected-target' },
],
prompts: ['Hello'],
tests: [{ vars: {} }],
});
await doEval({ table: false, write: false, config: [tempConfig] }, {}, undefined, {});
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(testSuite.providers.map((provider) => provider.label)).toEqual([
'excluded-target',
'selected-target',
]);
expect(evalRecord.runtimeOptions?.providerFilter).toBeUndefined();
expect(options).not.toHaveProperty('providerFilter');
});
it('should make commandLineOptions.filterSample repeatable with a configured seed', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-sample-seed.yaml', {
commandLineOptions: {
filterSample: 2,
filterSampleSeed: 42,
},
providers: ['echo'],
prompts: ['Hello {{input}}'],
tests: [
{ vars: { input: 'one' } },
{ vars: { input: 'two' } },
{ vars: { input: 'three' } },
],
});
await doEval({ table: false, write: false, config: [tempConfig] }, {}, undefined, {});
const firstSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
await doEval({ table: false, write: false, config: [tempConfig] }, {}, undefined, {});
const secondSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(firstSuite.tests?.map((test) => test.vars?.input)).toEqual(['two', 'three']);
expect(firstSuite.tests?.map((test) => test.vars?.input)).toEqual(
secondSuite.tests?.map((test) => test.vars?.input),
);
});
it('should use evaluateOptions.filterRange when command-line defaults do not set it', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-evaluate-options-filter-range.yaml', {
evaluateOptions: {
filterRange: '1:2',
},
providers: ['echo'],
prompts: ['Hello {{input}}'],
tests: [
{ vars: { input: 'one' } },
{ vars: { input: 'two' } },
{ vars: { input: 'three' } },
],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(testSuite.tests?.map((test) => test.vars?.input)).toEqual(['two']);
expect(evalRecord.runtimeOptions?.filterRange).toBe('1:2');
expect(options.filterRange).toBeUndefined();
});
it('should suppress the implicit default test when filters remove explicit tests', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-pattern-filter-empty.yaml', {
providers: ['echo'],
prompts: ['Hello {{input}}'],
tests: [{ description: 'kept only by matching filters', vars: { input: 'one' } }],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterPattern: 'no match',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(testSuite.tests).toHaveLength(0);
expect(testSuite.scenarios).toEqual([]);
});
it.each([
['--filter-pattern', { filterPattern: 'no match' }],
['--filter-metadata', { filterMetadata: 'category=drop' }],
])('should apply %s to the implicit default test', async (_filterName, filterOptions) => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-implicit-default.yaml', {
providers: ['echo'],
prompts: ['Hello'],
defaultTest: {
metadata: { category: 'keep' },
assert: [{ type: 'contains', value: 'Hello' }],
},
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
...filterOptions,
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(testSuite.tests).toHaveLength(0);
expect(testSuite.scenarios).toEqual([]);
});
it('should filter an implicit default test by inherited metadata', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-implicit-default-metadata.yaml', {
providers: ['echo'],
prompts: ['Hello'],
defaultTest: {
metadata: { category: 'keep' },
assert: [{ type: 'contains', value: 'Hello' }],
},
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterMetadata: 'category=keep',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(testSuite.tests).toHaveLength(1);
expect(testSuite.tests?.[0].metadata).toEqual({ category: 'keep' });
expect(testSuite.scenarios).toBeUndefined();
});
it('should apply filterRange to the implicit default test', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-range-implicit-default.yaml', {
providers: ['echo'],
prompts: ['Hello'],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterRange: '0:1',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(testSuite.tests).toHaveLength(1);
expect(evalRecord.runtimeOptions?.filterRange).toBe('0:1');
expect(options.filterRange).toBeUndefined();
});
it('should not synthesize a default test from explicitly empty scenarios', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-range-empty-scenarios.yaml', {
providers: ['echo'],
prompts: ['Hello'],
scenarios: [],
defaultTest: {
assert: [{ type: 'contains', value: 'Hello' }],
},
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterRange: '0:1',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
expect(testSuite.tests).toHaveLength(0);
expect(testSuite.scenarios).toEqual([]);
});
it('should preserve empty filterRange slices for the implicit default test', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-filter-range-implicit-empty.yaml', {
providers: ['echo'],
prompts: ['Hello'],
});
await doEval(
{
table: false,
write: false,
config: [tempConfig],
filterRange: '0:0',
},
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const testSuite = evaluateMock.mock.calls.at(-1)?.[0] as TestSuite;
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(testSuite.tests).toHaveLength(0);
expect(testSuite.scenarios).toEqual([]);
expect(evalRecord.runtimeOptions?.filterRange).toBe('0:0');
expect(options.filterRange).toBeUndefined();
});
it('should restore persisted filterRange when resuming scenario evals', async () => {
const resumeEval = new Eval(
{
providers: ['echo'],
prompts: ['Hello {{name}}'],
scenarios: [
{
config: [{}],
tests: [{ vars: { name: 'Alice' } }, { vars: { name: 'Bob' } }],
},
],
},
{
id: 'eval-resume-filter-range',
persisted: true,
runtimeOptions: {
cache: true,
filterRange: '1:2',
maxConcurrency: 1,
repeat: 1,
},
},
);
const findByIdSpy = vi.spyOn(Eval, 'findById').mockResolvedValue(resumeEval);
try {
await doEval(
{
table: false,
resume: 'eval-resume-filter-range',
} as any,
{},
undefined,
{},
);
expect(evaluateMock).toHaveBeenCalled();
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(options.filterRange).toBe('1:2');
} finally {
findByIdSpy.mockRestore();
}
});
it.each([
['commandLineOptions', { commandLineOptions: { filterRange: '1:2' } }],
['evaluateOptions', { evaluateOptions: { filterRange: '1:2' } }],
])('should restore legacy %s.filterRange when resuming evals without persisted runtime options', async (_source, legacyConfig) => {
const resumeEval = new Eval(
{
...legacyConfig,
providers: ['echo'],
prompts: ['Hello {{name}}'],
tests: [
{ vars: { name: 'Alice' } },
{ vars: { name: 'Bob' } },
{ vars: { name: 'Carol' } },
],
},
{
id: 'eval-resume-without-filter-range',
persisted: true,
},
);
const findByIdSpy = vi.spyOn(Eval, 'findById').mockResolvedValue(resumeEval);
try {
await doEval(
{
table: false,
resume: 'eval-resume-without-filter-range',
} as any,
{},
undefined,
{
filterRange: '0:1',
},
);
expect(evaluateMock).toHaveBeenCalled();
const options = evaluateMock.mock.calls.at(-1)?.[2] as EvaluateOptions;
expect(options.filterRange).toBe('1:2');
} finally {
findByIdSpy.mockRestore();
}
});
it('should warn and ignore CLI --filter-range when resuming with a different persisted range', async () => {
const resumeEval = new Eval(
{
providers: ['echo'],
prompts: ['Hello {{name}}'],
tests: [
{ vars: { name: 'Alice' } },
{ vars: { name: 'Bob' } },
{ vars: { name: 'Carol' } },
],
},
{
id: 'eval-resume-filter-range-conflict',
persisted: true,
runtimeOptions: {
cache: true,
filterRange: '0:1',
maxConcurrency: 1,
repeat: 1,
},
},
);
const findByIdSpy = vi.spyOn(Eval, 'findById').mockResolvedValue(resumeEval);
const warnMock = vi.mocked(logger.warn);
warnMock.mockClear();
try {
await doEval(
{
table: false,
resume: 'eval-resume-filter-range-conflict',
filterRange: '1:3',
} as any,
{},
undefined,
{},
);
expect(warnMock).toHaveBeenCalledWith(
expect.stringContaining('Ignoring --filter-range 1:3'),
);
const evalRecord = evaluateMock.mock.calls.at(-1)?.[1] as Eval;
expect(evalRecord.runtimeOptions?.filterRange).toBe('0:1');
} finally {
findByIdSpy.mockRestore();
}
});
it('should prioritize CLI --suggest-prompts over commandLineOptions.generateSuggestions', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-generate-suggestions-override.yaml', {
commandLineOptions: {
generateSuggestions: false,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
suggestPrompts: 3, // CLI override
} as Partial<CommandLineOptions & Command>);
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(3);
});
it('should let an explicit generateSuggestions=false override commandLineOptions config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-generate-suggestions-disabled.yaml', {
commandLineOptions: {
generateSuggestions: true,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
generateSuggestions: false,
});
expect(options.generateSuggestions).toBe(false);
expect(options.suggestionsCount).toBeUndefined();
});
it('should let an explicit generateSuggestions=false override evaluateOptions config', async () => {
const tempConfig = writeTempConfig(
tmpDir,
'test-generate-suggestions-disabled-evaluate.yaml',
{
evaluateOptions: {
generateSuggestions: true,
suggestionsCount: 4,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
},
);
const options = await runEvalAndGetOptions({
config: [tempConfig],
generateSuggestions: false,
});
expect(options.generateSuggestions).toBe(false);
expect(options.suggestionsCount).toBe(4);
});
it('should let commandLineOptions.generateSuggestions=false override evaluateOptions.generateSuggestions=true', async () => {
const tempConfig = writeTempConfig(
tmpDir,
'test-cli-options-disable-overrides-evaluate.yaml',
{
commandLineOptions: { generateSuggestions: false },
evaluateOptions: { generateSuggestions: true, suggestionsCount: 4 },
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
},
);
const options = await runEvalAndGetOptions({ config: [tempConfig] });
expect(options.generateSuggestions).toBe(false);
expect(options.suggestionsCount).toBe(4);
});
it('should honor cmdObj.suggestionsCount from non-Commander callers', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-cmdobj-suggestions-count.yaml', {
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
generateSuggestions: true,
suggestionsCount: 7,
});
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(7);
});
it('should let --suggest-prompts override an explicit generateSuggestions=false', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-suggest-prompts-overrides-disable.yaml', {
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const options = await runEvalAndGetOptions({
config: [tempConfig],
generateSuggestions: false,
suggestPrompts: 5,
} as Partial<CommandLineOptions & Command>);
expect(options.generateSuggestions).toBe(true);
expect(options.suggestionsCount).toBe(5);
});
it('should respect commandLineOptions.table from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-table.yaml', {
commandLineOptions: {
table: false,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const cmdObj: Partial<CommandLineOptions & Command> = {
write: false,
config: [tempConfig],
};
// We can't directly check table output, but we can verify the config loads
await doEval(cmdObj, {}, undefined, {});
// If this completes without error, the config was respected
expect(evaluateMock).toHaveBeenCalled();
});
it('should respect commandLineOptions.write = false from config', async () => {
const tempConfig = writeTempConfig(tmpDir, 'test-write.yaml', {
commandLineOptions: {
write: false,
},
providers: [{ id: 'openai:gpt-4o-mini' }],
prompts: ['Test prompt'],
tests: [{ vars: { input: 'test' } }],
});
const cmdObj: Partial<CommandLineOptions & Command> = {
table: false,
config: [tempConfig],
};
await doEval(cmdObj, {}, undefined, {});
// Verify eval completed successfully with write=false
expect(evaluateMock).toHaveBeenCalled();
});
});
});