Files
promptfoo--promptfoo/test/util/config/scenarios.test.ts
T
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

240 lines
7.8 KiB
TypeScript

import * as fs from 'fs';
import * as fsPromises from 'fs/promises';
import { globSync } from 'glob';
import * as yaml from 'js-yaml';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { validateAssertions } from '../../../src/assertions/validateAssertions';
import cliState from '../../../src/cliState';
import { readPrompts, readProviderPromptMap } from '../../../src/prompts/index';
import { loadApiProviders } from '../../../src/providers/index';
import { type TestCase } from '../../../src/types/index';
// Import after mocking
import { resolveConfigs } from '../../../src/util/config/load';
import { maybeLoadFromExternalFile } from '../../../src/util/file';
import { readFilters } from '../../../src/util/index';
import { readTests } from '../../../src/util/testCaseReader';
import { createMockProvider } from '../../factories/provider';
vi.mock('fs');
vi.mock('fs/promises');
vi.mock('glob', () => ({
globSync: vi.fn(),
hasMagic: vi.fn((pattern: string | string[]) => {
const p = Array.isArray(pattern) ? pattern.join('') : pattern;
return p.includes('*') || p.includes('?') || p.includes('[') || p.includes('{');
}),
}));
// Mock all the dependencies first
vi.mock('../../../src/util/file', async () => {
const actual =
await vi.importActual<typeof import('../../../src/util/file')>('../../../src/util/file');
return {
...actual,
maybeLoadFromExternalFile: vi.fn(),
};
});
vi.mock('../../../src/prompts');
vi.mock('../../../src/providers');
vi.mock('../../../src/util/testCaseReader');
vi.mock('../../../src/util', async () => {
const actual = await vi.importActual<typeof import('../../../src/util')>('../../../src/util');
return {
...actual,
readFilters: vi.fn(),
};
});
vi.mock('../../../src/assertions/validateAssertions');
describe('Scenario loading with glob patterns', () => {
const originalBasePath = cliState.basePath;
beforeEach(() => {
vi.clearAllMocks();
cliState.basePath = '/test/path';
// Setup default mocks
vi.mocked(readPrompts).mockResolvedValue([{ raw: 'Test prompt', label: 'Test prompt' }]);
vi.mocked(readProviderPromptMap).mockReturnValue({});
vi.mocked(loadApiProviders).mockResolvedValue([
createMockProvider({ id: 'openai:gpt-3.5-turbo' }),
]);
vi.mocked(readTests).mockImplementation(async (tests) =>
Array.isArray(tests) ? (tests as TestCase[]) : [],
);
vi.mocked(readFilters).mockResolvedValue({});
vi.mocked(validateAssertions).mockImplementation(() => {});
});
afterEach(() => {
cliState.basePath = originalBasePath;
});
it('should flatten scenarios when loaded with glob patterns', async () => {
const scenario1 = {
description: 'Scenario 1',
config: [{ vars: { name: 'Alice' } }],
tests: [{ vars: { question: 'Test 1' } }],
};
const scenario2 = {
description: 'Scenario 2',
config: [{ vars: { name: 'Bob' } }],
tests: [{ vars: { question: 'Test 2' } }],
};
// Mock file system
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fsPromises.readFile).mockImplementation((filePath) => {
if (filePath === 'config.yaml') {
return Promise.resolve(
yaml.dump({
prompts: ['Test prompt'],
providers: ['openai:gpt-3.5-turbo'],
scenarios: ['file://scenarios/*.yaml'],
}),
);
}
return Promise.resolve('');
});
// Mock glob to return config file
vi.mocked(globSync).mockReturnValue(['config.yaml']);
// Mock maybeLoadFromExternalFile to return nested array (simulating glob expansion)
vi.mocked(maybeLoadFromExternalFile).mockImplementation((input) => {
if (Array.isArray(input) && input[0] === 'file://scenarios/*.yaml') {
// Return nested array as would happen with glob pattern
return [[scenario1, scenario2]];
}
return input;
});
const cmdObj = { config: ['config.yaml'] };
const defaultConfig = {};
const { testSuite } = await resolveConfigs(cmdObj, defaultConfig);
// Check if maybeLoadFromExternalFile was called with the expected argument
expect(maybeLoadFromExternalFile).toHaveBeenCalledWith(['file://scenarios/*.yaml']);
// Verify scenarios are flattened correctly
expect(testSuite.scenarios).toHaveLength(2);
expect(testSuite.scenarios![0]).toEqual(scenario1);
expect(testSuite.scenarios![1]).toEqual(scenario2);
});
it('should handle multiple scenario files with glob patterns', async () => {
const scenarios = [
{
description: 'Scenario A',
config: [{ vars: { test: 'A' } }],
tests: [{ vars: { input: '1' } }],
},
{
description: 'Scenario B',
config: [{ vars: { test: 'B' } }],
tests: [{ vars: { input: '2' } }],
},
{
description: 'Scenario C',
config: [{ vars: { test: 'C' } }],
tests: [{ vars: { input: '3' } }],
},
];
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fsPromises.readFile).mockImplementation((filePath) => {
if (filePath === 'config.yaml') {
return Promise.resolve(
yaml.dump({
prompts: ['Test prompt'],
providers: ['openai:gpt-3.5-turbo'],
scenarios: ['file://group1/*.yaml', 'file://group2/*.yaml'],
}),
);
}
return Promise.resolve('');
});
vi.mocked(globSync).mockReturnValue(['config.yaml']);
vi.mocked(maybeLoadFromExternalFile).mockImplementation((input) => {
if (Array.isArray(input)) {
// Simulate two glob patterns each returning different scenarios
return [
[scenarios[0], scenarios[1]], // group1/*.yaml
[scenarios[2]], // group2/*.yaml
];
}
return input;
});
const cmdObj = { config: ['config.yaml'] };
const defaultConfig = {};
const { testSuite } = await resolveConfigs(cmdObj, defaultConfig);
// Verify all scenarios are flattened into a single array
expect(testSuite.scenarios).toHaveLength(3);
expect(testSuite.scenarios).toEqual(scenarios);
});
it('should handle mixed scenario loading (direct and glob)', async () => {
const directScenario = {
description: 'Direct scenario',
config: [{ vars: { type: 'direct' } }],
tests: [{ vars: { test: 'direct' } }],
};
const globScenarios = [
{
description: 'Glob scenario 1',
config: [{ vars: { type: 'glob' } }],
tests: [{ vars: { test: 'glob1' } }],
},
{
description: 'Glob scenario 2',
config: [{ vars: { type: 'glob' } }],
tests: [{ vars: { test: 'glob2' } }],
},
];
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fsPromises.readFile).mockImplementation((filePath) => {
if (filePath === 'config.yaml') {
return Promise.resolve(
yaml.dump({
prompts: ['Test prompt'],
providers: ['openai:gpt-3.5-turbo'],
scenarios: [directScenario, 'file://scenarios/*.yaml'],
}),
);
}
return Promise.resolve('');
});
vi.mocked(globSync).mockReturnValue(['config.yaml']);
vi.mocked(maybeLoadFromExternalFile).mockImplementation((input) => {
if (Array.isArray(input)) {
// First element is direct scenario, second is glob pattern
return [directScenario, globScenarios];
}
return input;
});
const cmdObj = { config: ['config.yaml'] };
const defaultConfig = {};
const { testSuite } = await resolveConfigs(cmdObj, defaultConfig);
// Verify mixed scenarios are flattened correctly
expect(testSuite.scenarios).toHaveLength(3);
expect(testSuite.scenarios![0]).toEqual(directScenario);
expect(testSuite.scenarios![1]).toEqual(globScenarios[0]);
expect(testSuite.scenarios![2]).toEqual(globScenarios[1]);
});
});