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

399 lines
13 KiB
TypeScript

import './setup';
import { randomUUID } from 'crypto';
import { expect, it, vi } from 'vitest';
import { evaluate } from '../../src/evaluator';
import Eval from '../../src/models/eval';
import { type ApiProvider, type TestSuite } from '../../src/types/index';
import { toPrompt } from './helpers';
import { describeEvaluator } from './lifecycle';
describeEvaluator('evaluator scenarios and conversations', () => {
it('evaluate with scenarios', async () => {
const mockApiProvider: ApiProvider = {
id: vi.fn().mockReturnValue('test-provider'),
callApi: vi
.fn()
.mockResolvedValueOnce({
output: 'Hola mundo',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
})
.mockResolvedValueOnce({
output: 'Bonjour le monde',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
}),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [toPrompt('Test prompt {{ language }}')],
scenarios: [
{
config: [
{
vars: {
language: 'Spanish',
expectedHelloWorld: 'Hola mundo',
},
},
{
vars: {
language: 'French',
expectedHelloWorld: 'Bonjour le monde',
},
},
],
tests: [
{
assert: [
{
type: 'equals',
value: '{{expectedHelloWorld}}',
},
],
},
],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, {});
const summary = await evalRecord.toEvaluateSummary();
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(2);
expect(summary.stats.successes).toBe(2);
expect(summary.stats.failures).toBe(0);
expect(summary.results[0].response?.output).toBe('Hola mundo');
expect(summary.results[1].response?.output).toBe('Bonjour le monde');
});
it('applies repeat from scenario config options', async () => {
const mockApiProvider: ApiProvider = {
id: vi.fn().mockReturnValue('test-provider'),
callApi: vi.fn().mockResolvedValue({
output: 'Hello',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
}),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [toPrompt('Test prompt')],
scenarios: [
{
config: [{ options: { repeat: 3 } }],
tests: [{}],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, { repeat: 1 });
const summary = await evalRecord.toEvaluateSummary();
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(3);
expect(summary.results).toHaveLength(3);
});
it('lets scenario test options.repeat override scenario config options.repeat', async () => {
const mockApiProvider: ApiProvider = {
id: vi.fn().mockReturnValue('test-provider'),
callApi: vi.fn().mockResolvedValue({
output: 'Hello',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
}),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [toPrompt('Test prompt')],
scenarios: [
{
config: [{ options: { repeat: 5 } }],
tests: [{ options: { repeat: 2 } }],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, { repeat: 1 });
const summary = await evalRecord.toEvaluateSummary();
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(2);
expect(summary.results).toHaveLength(2);
});
it('evaluate with scenarios and multiple vars', async () => {
const mockApiProvider: ApiProvider = {
id: vi.fn().mockReturnValue('test-provider'),
callApi: vi
.fn()
.mockResolvedValueOnce({
output: 'Spanish Hola',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
})
.mockResolvedValueOnce({
output: 'Spanish Bonjour',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
})
.mockResolvedValueOnce({
output: 'French Hola',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
})
.mockResolvedValueOnce({
output: 'French Bonjour',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
}),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [toPrompt('Test prompt {{ language }} {{ greeting }}')],
scenarios: [
{
config: [
{
vars: {
language: ['Spanish', 'French'],
greeting: ['Hola', 'Bonjour'],
},
},
],
tests: [
{
assert: [
{
type: 'equals',
value: '{{language}} {{greeting}}',
},
],
},
],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, {});
const summary = await evalRecord.toEvaluateSummary();
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(4);
expect(summary.stats.successes).toBe(4);
expect(summary.stats.failures).toBe(0);
expect(summary.results[0].response?.output).toBe('Spanish Hola');
expect(summary.results[1].response?.output).toBe('Spanish Bonjour');
expect(summary.results[2].response?.output).toBe('French Hola');
expect(summary.results[3].response?.output).toBe('French Bonjour');
});
it('evaluate with scenarios and defaultTest', async () => {
const mockApiProvider: ApiProvider = {
id: vi.fn().mockReturnValue('test-provider'),
callApi: vi.fn().mockResolvedValue({
output: 'Hello, World',
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
}),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [toPrompt('Test prompt')],
defaultTest: {
metadata: { defaultKey: 'defaultValue' },
assert: [
{
type: 'starts-with',
value: 'Hello',
},
],
},
scenarios: [
{
config: [{ metadata: { configKey: 'configValue' } }],
tests: [{ metadata: { testKey: 'testValue' } }],
},
{
config: [
{
assert: [
{
type: 'contains',
value: ',',
},
],
},
],
tests: [
{
assert: [
{
type: 'icontains',
value: 'world',
},
],
},
],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, {});
const summary = await evalRecord.toEvaluateSummary();
expect(summary).toMatchObject({
stats: {
successes: 2,
failures: 0,
},
results: expect.arrayContaining([
expect.objectContaining({
gradingResult: expect.objectContaining({
componentResults: expect.arrayContaining([expect.anything()]),
}),
}),
expect.objectContaining({
gradingResult: expect.objectContaining({
componentResults: expect.arrayContaining([
expect.anything(),
expect.anything(),
expect.anything(),
]),
}),
}),
]),
});
expect(summary.results[0].testCase.metadata).toEqual({
defaultKey: 'defaultValue',
configKey: 'configValue',
testKey: 'testValue',
conversationId: '__scenario_0__', // Auto-generated for scenario isolation
});
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(2);
});
it('should maintain separate conversation histories between scenarios without explicit conversationId', async () => {
// This test verifies the fix for GitHub issue #384:
// Scenarios should have isolated _conversation state by default
const mockApiProvider = {
id: () => 'test-provider',
callApi: vi.fn().mockImplementation((_prompt) => ({
output: 'Test output',
})),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [
{
raw: '{% for completion in _conversation %}Previous: {{ completion.input }} -> {{ completion.output }}\n{% endfor %}Current: {{ question }}',
label: 'Conversation test',
},
],
scenarios: [
{
// First scenario - conversation about books
config: [{}],
tests: [
{ vars: { question: 'Recommend a sci-fi book' } },
{ vars: { question: 'Tell me more about it' } },
],
},
{
// Second scenario - conversation about recipes
// Should NOT include history from first scenario
config: [{}],
tests: [
{ vars: { question: 'Suggest a pasta recipe' } },
{ vars: { question: 'How long does it take?' } },
],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, {});
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(4);
// First scenario, first question - no history
const firstCall = mockApiProvider.callApi.mock.calls[0][0];
expect(firstCall).toContain('Current: Recommend a sci-fi book');
expect(firstCall).not.toContain('Previous:');
// First scenario, second question - should have first scenario's history
const secondCall = mockApiProvider.callApi.mock.calls[1][0];
expect(secondCall).toContain('Previous: ');
expect(secondCall).toContain('Recommend a sci-fi book');
expect(secondCall).toContain('Current: Tell me more about it');
// Second scenario, first question - should NOT have first scenario's history
// This is the key assertion that verifies the fix for issue #384
const thirdCall = mockApiProvider.callApi.mock.calls[2][0];
expect(thirdCall).toContain('Current: Suggest a pasta recipe');
expect(thirdCall).not.toContain('Previous:');
expect(thirdCall).not.toContain('sci-fi');
expect(thirdCall).not.toContain('Recommend');
// Second scenario, second question - should only have second scenario's history
const fourthCall = mockApiProvider.callApi.mock.calls[3][0];
expect(fourthCall).toContain('Previous: ');
expect(fourthCall).toContain('Suggest a pasta recipe');
expect(fourthCall).toContain('Current: How long does it take?');
expect(fourthCall).not.toContain('sci-fi');
});
it('should allow scenarios to share conversation history with explicit conversationId', async () => {
// This test verifies that users can still explicitly share conversations across scenarios
const mockApiProvider = {
id: () => 'test-provider',
callApi: vi.fn().mockImplementation((_prompt) => ({
output: 'Test output',
})),
};
const testSuite: TestSuite = {
providers: [mockApiProvider],
prompts: [
{
raw: '{% for completion in _conversation %}Previous: {{ completion.input }}\n{% endfor %}Current: {{ question }}',
label: 'Conversation test',
},
],
scenarios: [
{
config: [{}],
tests: [
{
vars: { question: 'Question from scenario 1' },
metadata: { conversationId: 'shared-conversation' },
},
],
},
{
config: [{}],
tests: [
{
vars: { question: 'Question from scenario 2' },
metadata: { conversationId: 'shared-conversation' },
},
],
},
],
};
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
await evaluate(testSuite, evalRecord, {});
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(2);
// First scenario - no history
const firstCall = mockApiProvider.callApi.mock.calls[0][0];
expect(firstCall).not.toContain('Previous:');
// Second scenario - SHOULD have first scenario's history because they share conversationId
const secondCall = mockApiProvider.callApi.mock.calls[1][0];
expect(secondCall).toContain('Previous: ');
expect(secondCall).toContain('Question from scenario 1');
});
});