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

171 lines
6.1 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
buildConfiguredProviderMap,
GRADING_PROVIDER_TYPE_KEYS,
isProviderTypeMap,
resolveConfiguredProviderReference,
} from '../../src/util/gradingProvider';
import type { ApiProvider } from '../../src/types/providers';
function makeProvider(id: string, label?: string): ApiProvider {
return {
id: vi.fn().mockReturnValue(id),
label,
callApi: vi.fn().mockResolvedValue({ output: '' }),
};
}
describe('GRADING_PROVIDER_TYPE_KEYS', () => {
it('lists the four supported grading-provider types', () => {
expect([...GRADING_PROVIDER_TYPE_KEYS]).toEqual([
'text',
'embedding',
'classification',
'moderation',
]);
});
});
describe('isProviderTypeMap', () => {
it.each([
['null', null, false],
['undefined', undefined, false],
['string', 'litellm:judge', false],
['array', ['litellm:judge'], false],
['empty object', {}, false],
['ProviderOptions (has id)', { id: 'litellm:judge', config: {} }, false],
[
'ApiProvider instance',
{ id: () => 'x', callApi: () => Promise.resolve({ output: '' }) },
false,
],
['ProviderOptionsMap (unknown key)', { 'litellm:judge': { config: {} } }, false],
['ProviderOptionsMap (type-named key)', { text: { config: {} } }, false],
['type key with empty string value', { text: '' }, false],
['type key with undefined value', { text: undefined }, false],
])('returns false for %s', (_label, value, expected) => {
expect(isProviderTypeMap(value)).toBe(expected);
});
it.each([
['text', { text: 'litellm:judge' }],
['text ProviderOptions', { text: { id: 'litellm:judge', config: {} } }],
['embedding', { embedding: 'litellm:embed' }],
['classification', { classification: 'classifier' }],
['moderation', { moderation: 'moderator' }],
['multiple', { text: 'litellm:judge', embedding: 'litellm:embed' }],
])('returns true for typed map (%s)', (_label, value) => {
expect(isProviderTypeMap(value)).toBe(true);
});
});
describe('buildConfiguredProviderMap', () => {
it('returns a null-prototype map', () => {
const map = buildConfiguredProviderMap([makeProvider('a')]);
expect(Object.getPrototypeOf(map)).toBeNull();
});
it('indexes providers by id and label', () => {
const provider = makeProvider('openai:gpt-4', 'gpt');
const map = buildConfiguredProviderMap([provider]);
expect(map['openai:gpt-4']).toBe(provider);
expect(map.gpt).toBe(provider);
});
it('does not let a later provider label shadow an earlier provider id', () => {
const byId = makeProvider('judge');
const byLabel = makeProvider('litellm:judge', 'judge');
const map = buildConfiguredProviderMap([byId, byLabel]);
expect(map.judge).toBe(byId);
expect(map['litellm:judge']).toBe(byLabel);
});
it('is independent of provider iteration order for id/label collisions', () => {
const byId = makeProvider('judge');
const byLabel = makeProvider('litellm:judge', 'judge');
const reverse = buildConfiguredProviderMap([byLabel, byId]);
expect(reverse.judge).toBe(byId);
expect(reverse['litellm:judge']).toBe(byLabel);
});
it('still applies a label alias when no id collides', () => {
const provider = makeProvider('litellm:judge', 'judge');
const map = buildConfiguredProviderMap([provider]);
expect(map.judge).toBe(provider);
expect(map['litellm:judge']).toBe(provider);
});
it('does not surface prototype properties through hasOwn lookups', () => {
const map = buildConfiguredProviderMap([makeProvider('a')]);
expect(Object.hasOwn(map, '__proto__')).toBe(false);
expect(Object.hasOwn(map, 'constructor')).toBe(false);
expect(Object.hasOwn(map, 'toString')).toBe(false);
});
it('returns an empty null-prototype map for an empty input', () => {
const map = buildConfiguredProviderMap([]);
expect(Object.getPrototypeOf(map)).toBeNull();
expect(Object.keys(map)).toEqual([]);
});
});
describe('resolveConfiguredProviderReference', () => {
it('preserves a fully resolved provider map when no entry needs env injection', () => {
const provider = makeProvider('litellm:judge');
const typeMap = { text: provider };
expect(
resolveConfiguredProviderReference(typeMap, buildConfiguredProviderMap([]), {
API_KEY: 'suite-key',
}),
).toBe(typeMap);
});
it('adds env only when a deferred typed provider must still be loaded', () => {
const embeddingProvider = makeProvider('litellm:embedding:judge');
const typeMap = { text: 'litellm:judge', embedding: embeddingProvider };
expect(
resolveConfiguredProviderReference(typeMap, buildConfiguredProviderMap([]), {
API_KEY: 'suite-key',
}),
).toEqual({
text: { id: 'litellm:judge', env: { API_KEY: 'suite-key' } },
embedding: embeddingProvider,
});
});
it('resolves configured typed entries while leaving unconfigured alternatives lazy', () => {
const textProvider = makeProvider('litellm:judge');
const providerMap = buildConfiguredProviderMap([textProvider]);
expect(
resolveConfiguredProviderReference(
{ text: 'litellm:judge', embedding: 'unsupported-provider:unused-embedding' },
providerMap,
),
).toEqual({
text: textProvider,
embedding: 'unsupported-provider:unused-embedding',
});
});
it('resolves an id-only typed provider option to a configured provider instance', () => {
const textProvider = makeProvider('litellm:judge');
const providerMap = buildConfiguredProviderMap([textProvider]);
expect(
resolveConfiguredProviderReference({ text: { id: 'litellm:judge' } }, providerMap),
).toEqual({ text: textProvider });
});
it('keeps a typed provider option with overrides inline', () => {
const textProvider = makeProvider('litellm:judge');
const providerMap = buildConfiguredProviderMap([textProvider]);
const inlineProvider = { text: { id: 'litellm:judge', config: { temperature: 0 } } };
expect(resolveConfiguredProviderReference(inlineProvider, providerMap)).toBe(inlineProvider);
});
});