chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
// Shared mock setup and lifecycle hooks for all HTTP provider test files.
import { afterAll, afterEach, beforeEach, vi } from 'vitest';
import { fetchWithCache } from '../../../src/cache';
import { runPython } from '../../../src/python/pythonUtils';
import { maybeLoadConfigFromExternalFile, maybeLoadFromExternalFile } from '../../../src/util/file';
import { functionCache } from '../../../src/util/functions/loadFunction';
// Mock console.warn to prevent test noise
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(function () {});
vi.mock('../../../src/cache', async () => {
const actual = await vi.importActual<typeof import('../../../src/cache')>('../../../src/cache');
return {
...actual,
fetchWithCache: vi.fn(),
};
});
vi.mock('../../../src/util/fetch/index.ts', async () => {
const actual = await vi.importActual<typeof import('../../../src/util/fetch/index.ts')>(
'../../../src/util/fetch/index.ts',
);
return {
...actual,
fetchWithRetries: vi.fn(),
fetchWithTimeout: vi.fn(),
};
});
vi.mock('../../../src/util/file', async () => {
const actual =
await vi.importActual<typeof import('../../../src/util/file')>('../../../src/util/file');
return {
...actual,
maybeLoadFromExternalFile: vi.fn((input) => input),
maybeLoadConfigFromExternalFile: vi.fn((input) => input),
};
});
vi.mock('../../../src/esm', async (importOriginal) => {
return {
...(await importOriginal()),
importModule: vi.fn(async (_modulePath: string, functionName?: string) => {
const mockModule: Record<string, ReturnType<typeof vi.fn>> = {
default: vi.fn((data) => data.defaultField),
parseResponse: vi.fn((data) => data.specificField),
};
if (functionName) {
if (!(functionName in mockModule)) {
throw new Error(
`importModule mock: unexpected functionName "${functionName}". ` +
'Add it to the mockModule in setup.ts or use mockResolvedValueOnce.',
);
}
return mockModule[functionName];
}
return mockModule.default;
}),
};
});
vi.mock('../../../src/cliState', async () => {
const actual =
await vi.importActual<typeof import('../../../src/cliState')>('../../../src/cliState');
const mockState = { basePath: '/mock/base/path', config: {} };
return {
...actual,
...mockState,
default: mockState,
};
});
vi.mock('../../../src/python/pythonUtils', async () => {
const actual = await vi.importActual<typeof import('../../../src/python/pythonUtils')>(
'../../../src/python/pythonUtils',
);
return {
...actual,
runPython: vi.fn(),
};
});
// Mock jks-js module for JKS keystore tests in auth.test.ts - don't use importOriginal as the native module may fail to load
vi.mock('jks-js', () => ({
toPem: vi.fn(),
default: {
toPem: vi.fn(),
},
}));
afterAll(() => {
consoleSpy.mockRestore();
});
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fetchWithCache).mockReset();
vi.mocked(maybeLoadFromExternalFile).mockReset();
vi.mocked(maybeLoadConfigFromExternalFile).mockReset();
vi.mocked(runPython).mockReset();
vi.mocked(fetchWithCache).mockImplementation(() => {
throw new Error(
'fetchWithCache called without mockResolvedValueOnce setup. ' +
'Add vi.mocked(fetchWithCache).mockResolvedValueOnce({...}) before calling provider.callApi().',
);
});
vi.mocked(maybeLoadFromExternalFile).mockImplementation(function (input: unknown) {
return input;
});
vi.mocked(maybeLoadConfigFromExternalFile).mockImplementation(function (input: unknown) {
return input;
});
Object.keys(functionCache).forEach((key) => {
delete functionCache[key];
});
});
afterEach(() => {
vi.resetAllMocks();
});
+663
View File
@@ -0,0 +1,663 @@
// Tools and template variable tests: tool_choice handling, transformToolsFormat integration.
import './setup';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchWithCache } from '../../../src/cache';
import logger from '../../../src/logger';
import { HttpProvider } from '../../../src/providers/http';
describe('tools and tool_choice template variables', () => {
const mockUrl = 'http://example.com/api';
beforeEach(() => {
vi.clearAllMocks();
});
it('should make tools available as a template variable', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
},
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather for a location',
parameters: { type: 'object', properties: { location: { type: 'string' } } },
},
},
];
await provider.callApi('test prompt', {
vars: {},
prompt: {
raw: 'test prompt',
label: 'test',
config: { tools },
},
});
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tools,
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should make tool_choice available as a template variable', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tool_choice: '{{ tool_choice }}',
},
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
const tool_choice = { type: 'function', function: { name: 'get_weather' } };
await provider.callApi('test prompt', {
vars: {},
prompt: {
raw: 'test prompt',
label: 'test',
config: { tool_choice },
},
});
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tool_choice,
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should handle both tools and tool_choice together', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
tool_choice: '{{ tool_choice }}',
},
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
const tools = [
{
type: 'function',
function: {
name: 'report_scores',
parameters: { type: 'object', properties: { score: { type: 'integer' } } },
},
},
];
const tool_choice = { type: 'function', function: { name: 'report_scores' } };
await provider.callApi('test prompt', {
vars: {},
prompt: {
raw: 'test prompt',
label: 'test',
config: { tools, tool_choice },
},
});
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tools,
tool_choice,
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should handle undefined tools gracefully', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
},
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
// No tools or tool_choice in config
await provider.callApi('test prompt', {
vars: {},
prompt: {
raw: 'test prompt',
label: 'test',
},
});
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
});
describe('transformToolsFormat integration', () => {
const mockUrl = 'http://example.com/api';
it('should pass through OpenAI tools unchanged when format is openai', async () => {
const openaiTools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
},
required: ['location'],
},
},
},
];
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
},
tools: openaiTools,
transformToolsFormat: 'openai',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tools: openaiTools,
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should transform OpenAI tools to anthropic format', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
},
tools: [
{
type: 'function',
function: {
name: 'search',
description: 'Search the web',
parameters: {
type: 'object',
properties: {
query: { type: 'string' },
},
},
},
},
],
transformToolsFormat: 'anthropic',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tools: [
{
name: 'search',
description: 'Search the web',
input_schema: {
type: 'object',
properties: {
query: { type: 'string' },
},
},
},
],
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should transform tool_choice to openai format', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
tool_choice: '{{ tool_choice }}',
},
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather',
},
},
],
tool_choice: { type: 'function', function: { name: 'get_weather' } },
transformToolsFormat: 'openai',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
messages: 'test prompt',
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather',
},
},
],
tool_choice: { type: 'function', function: { name: 'get_weather' } },
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should transform tool_choice mode required to anthropic format', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
tool_choice: '{{ tool_choice }}',
},
tools: [{ type: 'function', function: { name: 'my_tool' } }],
tool_choice: 'required',
transformToolsFormat: 'anthropic',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
body: JSON.stringify({
messages: 'test prompt',
tools: [
{
name: 'my_tool',
input_schema: { type: 'object', properties: {} },
},
],
tool_choice: { type: 'any' },
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should pass through non-OpenAI tools unchanged', async () => {
const anthropicTools = [
{
name: 'existing_tool',
description: 'Already in Anthropic format',
input_schema: { type: 'object' },
},
];
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
},
tools: anthropicTools,
transformToolsFormat: 'anthropic', // Won't transform - not in OpenAI format
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
body: JSON.stringify({
messages: 'test prompt',
tools: anthropicTools, // Unchanged
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should use tools from prompt.config over provider config', async () => {
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
},
tools: [{ type: 'function', function: { name: 'provider_tool' } }],
transformToolsFormat: 'anthropic',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
// prompt.config.tools should override provider config
await provider.callApi('test prompt', {
vars: {},
prompt: {
raw: 'test prompt',
label: 'test',
config: {
tools: [
{ type: 'function', function: { name: 'prompt_tool', description: 'From prompt' } },
],
},
},
});
expect(fetchWithCache).toHaveBeenCalledWith(
mockUrl,
expect.objectContaining({
body: JSON.stringify({
messages: 'test prompt',
tools: [
{
name: 'prompt_tool',
description: 'From prompt',
input_schema: { type: 'object', properties: {} },
},
],
}),
}),
expect.any(Number),
'text',
undefined,
undefined,
);
});
it('should warn when tool_choice is set but tools is empty', async () => {
const loggerWarnSpy = vi.spyOn(logger, 'warn');
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tool_choice: '{{ tool_choice }}',
},
tool_choice: 'required',
transformToolsFormat: 'openai',
// No tools configured
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(loggerWarnSpy).toHaveBeenCalled();
expect(loggerWarnSpy.mock.calls[0][0]).toEqual(
expect.stringContaining('tool_choice is set but tools is empty'),
);
loggerWarnSpy.mockRestore();
});
it('should warn when tool_choice is set but tools array is empty', async () => {
const loggerWarnSpy = vi.spyOn(logger, 'warn');
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
tool_choice: '{{ tool_choice }}',
},
tools: [], // Empty array
tool_choice: 'auto',
transformToolsFormat: 'openai',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(loggerWarnSpy).toHaveBeenCalled();
expect(loggerWarnSpy.mock.calls[0][0]).toEqual(
expect.stringContaining('tool_choice is set but tools is empty'),
);
loggerWarnSpy.mockRestore();
});
it('should not warn when both tools and tool_choice are set', async () => {
const loggerWarnSpy = vi.spyOn(logger, 'warn');
const provider = new HttpProvider(mockUrl, {
config: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
messages: '{{ prompt }}',
tools: '{{ tools }}',
tool_choice: '{{ tool_choice }}',
},
tools: [{ normalized: true, name: 'my_tool' }],
tool_choice: 'auto',
transformToolsFormat: 'openai',
},
});
const mockResponse = {
data: JSON.stringify({ result: 'success' }),
status: 200,
statusText: 'OK',
cached: false,
};
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
await provider.callApi('test prompt');
expect(
loggerWarnSpy.mock.calls.some(([message]) =>
String(message).includes('tool_choice is set but tools is empty'),
),
).toBe(false);
loggerWarnSpy.mockRestore();
});
});