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

506 lines
16 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createMockProvider as createFactoryProvider } from '../factories/provider';
import type { ApiProvider } from '../../src/types/providers';
// Use vi.hoisted to create mock functions that persist across clearAllMocks
const mockEvaluate = vi.hoisted(() => vi.fn());
const mockToEvaluateSummary = vi.hoisted(() =>
vi.fn().mockResolvedValue({
results: [
{
response: {
output: 'Hello! How can I help you?',
raw: 'Hello! How can I help you?',
sessionId: 'session-123',
metadata: { transformedRequest: { body: '{}' } },
},
error: undefined,
},
],
}),
);
const mockNeverGenerateRemote = vi.hoisted(() => vi.fn().mockReturnValue(false));
const mockFetchWithProxy = vi.hoisted(() =>
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
message: 'Test completed successfully',
error: null,
changes_needed: false,
}),
}),
);
const mockDoRemoteGrading = vi.hoisted(() =>
vi.fn().mockResolvedValue({
pass: true,
reason: 'Session is working correctly',
}),
);
const mockDetermineEffectiveSessionSource = vi.hoisted(() => vi.fn().mockReturnValue('client'));
vi.mock('dedent', () => ({
default: (strings: TemplateStringsArray, ...values: unknown[]) => {
let result = '';
strings.forEach((str, i) => {
result += str + (values[i] ?? '');
});
return result;
},
}));
vi.mock('../../src/evaluator', () => ({
evaluate: mockEvaluate,
}));
vi.mock('../../src/globalConfig/cloud', () => ({
cloudConfig: {
getApiHost: vi.fn().mockReturnValue('https://api.example.com'),
getApiKey: vi.fn().mockReturnValue('test-api-key'),
},
}));
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
vi.mock('../../src/models/eval', () => {
return {
default: class MockEval {
toEvaluateSummary = mockToEvaluateSummary;
},
};
});
vi.mock('../../src/redteam/remoteGeneration', () => ({
neverGenerateRemote: mockNeverGenerateRemote,
}));
vi.mock('../../src/remoteGrading', () => ({
doRemoteGrading: mockDoRemoteGrading,
}));
vi.mock('../../src/util/fetch/index', () => ({
fetchWithProxy: mockFetchWithProxy,
}));
vi.mock('../../src/util/sanitizer', () => ({
sanitizeObject: vi.fn((obj: unknown) => obj),
}));
vi.mock('../../src/validators/util', () => ({
determineEffectiveSessionSource: mockDetermineEffectiveSessionSource,
formatConfigBody: vi.fn().mockReturnValue('None configured'),
formatConfigHeaders: vi.fn().mockReturnValue('None configured'),
validateSessionConfig: vi.fn(),
}));
import { testProviderConnectivity, testProviderSession } from '../../src/node/testProvider';
beforeEach(() => {
// Reset call history but keep implementations from vi.hoisted
mockEvaluate.mockReset();
mockNeverGenerateRemote.mockReset().mockReturnValue(false);
mockDetermineEffectiveSessionSource.mockReset().mockReturnValue('client');
mockDoRemoteGrading.mockReset().mockResolvedValue({
pass: true,
reason: 'Session is working correctly',
});
mockToEvaluateSummary.mockReset().mockResolvedValue({
results: [
{
response: {
output: 'Hello! How can I help you?',
raw: 'Hello! How can I help you?',
sessionId: 'session-123',
metadata: { transformedRequest: { body: '{}' } },
},
error: undefined,
},
],
});
mockFetchWithProxy.mockReset().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
message: 'Test completed successfully',
error: null,
changes_needed: false,
}),
});
});
afterEach(() => {
vi.clearAllMocks();
});
function createMockProvider(overrides?: Record<string, unknown>): ApiProvider {
return Object.assign(
createFactoryProvider({
response: {
output: 'Hello! How can I help you?',
sessionId: 'session-123',
},
config: {},
}),
overrides,
) as unknown as ApiProvider;
}
describe('testProviderConnectivity', () => {
it('should return success when provider evaluation succeeds', async () => {
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(true);
expect(result.message).toBe('Test completed successfully');
expect(mockEvaluate).toHaveBeenCalled();
});
it('should use custom prompt when provided', async () => {
const provider = createMockProvider();
await testProviderConnectivity({ provider, prompt: 'Custom prompt' });
expect(mockEvaluate).toHaveBeenCalled();
const callArgs = mockEvaluate.mock.calls[0];
expect(callArgs[0].prompts[0].raw).toBe('Custom prompt');
});
it('should generate test values for input variables', async () => {
const provider = createMockProvider();
await testProviderConnectivity({
provider,
inputs: { name: 'User name', topic: 'Discussion topic' },
});
expect(mockEvaluate).toHaveBeenCalled();
const callArgs = mockEvaluate.mock.calls[0];
const vars = callArgs[0].tests[0].vars;
expect(vars['name']).toBe('test_name');
expect(vars['topic']).toBe('test_topic');
});
it('should return raw result when remote grading is disabled', async () => {
mockNeverGenerateRemote.mockReturnValue(true);
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(true);
expect(result.message).toContain('Remote grading disabled');
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should return error when evaluation result has error', async () => {
mockNeverGenerateRemote.mockReturnValue(true);
mockToEvaluateSummary.mockResolvedValueOnce({
results: [
{
response: { output: null },
error: 'Connection refused',
},
],
});
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Connection refused');
});
it('should handle evaluation throwing an error', async () => {
mockEvaluate.mockRejectedValueOnce(new Error('Evaluation failed'));
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(false);
expect(result.error).toBe('Evaluation failed');
});
it('should handle remote analysis endpoint failure', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
statusText: 'Internal Server Error',
} as any);
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('review the provider response manually');
});
it('should handle remote analysis endpoint throwing', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Network error'));
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(false);
expect(result.error).toBe('Network error');
});
it('should report changes_needed from analysis', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: vi.fn().mockResolvedValue({
message: 'Configuration needs changes',
error: null,
changes_needed: true,
changes_needed_reason: 'Missing auth header',
changes_needed_suggestions: ['Add Authorization header'],
}),
} as any);
const provider = createMockProvider();
const result = await testProviderConnectivity({ provider });
expect(result.success).toBe(false);
expect(result.analysis).toBeDefined();
expect(result.analysis?.changes_needed).toBe(true);
expect(result.analysis?.changes_needed_reason).toBe('Missing auth header');
});
it('should not set sessionId var when provider has sessionParser config', async () => {
const provider = createMockProvider({
config: { sessionParser: 'response.headers.session' },
});
await testProviderConnectivity({ provider });
expect(mockEvaluate).toHaveBeenCalled();
const callArgs = mockEvaluate.mock.calls[0];
const vars = callArgs[0].tests[0].vars;
expect(vars).not.toHaveProperty('sessionId');
});
it('should set sessionId var when provider has no sessionParser config', async () => {
const provider = createMockProvider();
await testProviderConnectivity({ provider });
expect(mockEvaluate).toHaveBeenCalled();
const callArgs = mockEvaluate.mock.calls[0];
const vars = callArgs[0].tests[0].vars;
expect(vars).toHaveProperty('sessionId');
});
it('should use default prompt when none provided', async () => {
const provider = createMockProvider();
await testProviderConnectivity({ provider });
const callArgs = mockEvaluate.mock.calls[0];
expect(callArgs[0].prompts[0].raw).toBe('Hello World!');
});
});
describe('testProviderSession', () => {
it('should return success when session is working', async () => {
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.success).toBe(true);
expect(result.message).toContain('working correctly');
});
it('should return failure when first request fails', async () => {
const provider = createMockProvider({
callApi: vi.fn().mockResolvedValue({
error: 'Connection timeout',
output: null,
}),
});
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('First request failed');
expect(result.error).toBe('Connection timeout');
});
it('should return failure when second request fails', async () => {
const callApi = vi
.fn()
.mockResolvedValueOnce({
output: 'Hello! I can help with many things.',
sessionId: 'session-123',
})
.mockResolvedValueOnce({
error: 'Server error',
output: null,
});
const provider = createMockProvider({ callApi });
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Second request failed');
});
it('should return manual review message when remote grading is disabled', async () => {
mockNeverGenerateRemote.mockReturnValue(true);
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Remote grading is disabled');
});
it('should return failure when session grading fails', async () => {
mockDoRemoteGrading.mockRejectedValueOnce(new Error('Grading service unavailable'));
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Failed to evaluate session');
});
it('should return failure when session is not working', async () => {
mockDoRemoteGrading.mockResolvedValueOnce({
pass: false,
score: 0,
reason: 'Provider did not remember the previous question',
});
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Session is NOT working');
});
it('should handle server session source with extraction failure', async () => {
mockDetermineEffectiveSessionSource.mockReturnValue('server');
const provider = createMockProvider({
callApi: vi.fn().mockResolvedValue({
output: 'Hello!',
// No sessionId returned
}),
getSessionId: vi.fn().mockReturnValue(undefined),
});
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.message).toContain('Session extraction failed');
});
it('should handle input variables for multi-input configurations', async () => {
const callApi = vi.fn().mockResolvedValue({
output: 'Response',
sessionId: 'session-123',
});
const provider = createMockProvider({ callApi });
await testProviderSession({
provider,
inputs: { user_message: 'Main input', context: 'Additional context' },
mainInputVariable: 'user_message',
});
expect(callApi).toHaveBeenCalledTimes(2);
const firstContext = callApi.mock.calls[0][1];
expect(firstContext.vars).toHaveProperty('context', 'test_context');
expect(firstContext.vars).toHaveProperty('user_message', 'What can you help me with?');
});
it('should materialize typed main input variables during session tests', async () => {
const callApi = vi.fn().mockResolvedValue({
output: 'Response',
sessionId: 'session-123',
});
const provider = createMockProvider({ callApi });
await testProviderSession({
provider,
inputs: {
document: {
description: 'Uploaded document',
type: 'docx',
},
question: 'Question',
},
mainInputVariable: 'document',
});
expect(callApi).toHaveBeenCalledTimes(2);
const firstContext = callApi.mock.calls[0][1];
const secondContext = callApi.mock.calls[1][1];
expect(firstContext.vars.document).toMatch(
/^data:application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document;base64,/,
);
expect(secondContext.vars.document).toMatch(
/^data:application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document;base64,/,
);
expect(firstContext.vars.document).not.toBe('What can you help me with?');
expect(secondContext.vars.document).not.toBe('What was the last thing I asked you?');
});
it('should handle errors thrown during session test', async () => {
const provider = createMockProvider({
callApi: vi.fn().mockRejectedValue(new Error('Unexpected error')),
});
const result = await testProviderSession({ provider });
expect(result.success).toBe(false);
expect(result.error).toBe('Unexpected error');
});
it('should include session details in successful response', async () => {
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.details).toBeDefined();
expect(result.details?.sessionSource).toBe('client');
expect(result.details?.request1).toBeDefined();
expect(result.details?.response1).toBeDefined();
expect(result.details?.request2).toBeDefined();
expect(result.details?.response2).toBeDefined();
});
it('should include reason from grading judge', async () => {
mockDoRemoteGrading.mockResolvedValueOnce({
pass: true,
reason: 'The model remembered the first question',
});
const provider = createMockProvider();
const result = await testProviderSession({ provider });
expect(result.success).toBe(true);
expect(result.reason).toBe('The model remembered the first question');
});
it('should generate dummy values for non-main input variables', async () => {
const callApi = vi.fn().mockResolvedValue({
output: 'Response',
sessionId: 'session-123',
});
const provider = createMockProvider({ callApi });
await testProviderSession({
provider,
inputs: {
user_message: 'Main',
system_prompt: 'System instruction',
language: 'Language preference',
},
mainInputVariable: 'user_message',
});
const firstContext = callApi.mock.calls[0][1];
expect(firstContext.vars).toHaveProperty('system_prompt', 'test_system_prompt');
expect(firstContext.vars).toHaveProperty('language', 'test_language');
// main input variable should have the actual prompt
expect(firstContext.vars).toHaveProperty('user_message', 'What can you help me with?');
});
});