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

601 lines
17 KiB
TypeScript

import fs from 'fs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { runAssertion } from '../../src/assertions/index';
import { AIStudioChatProvider } from '../../src/providers/google/ai.studio';
import { GoogleLiveProvider } from '../../src/providers/google/live';
import { GoogleProvider } from '../../src/providers/google/provider';
import { validateFunctionCall } from '../../src/providers/google/util';
import { VertexChatProvider } from '../../src/providers/google/vertex';
import { createMockProvider } from '../factories/provider';
import type { Tool } from '../../src/providers/google/types';
import type { ApiProvider, AtomicTestCase, GradingResult } from '../../src/types/index';
// Create hoisted mocks for stable references
const mocks = vi.hoisted(() => ({
mockPathResolve: vi.fn(),
}));
vi.mock('fs');
vi.mock('path', async () => {
const actual = await vi.importActual<typeof import('path')>('path');
return {
...actual,
resolve: mocks.mockPathResolve,
};
});
const mockedFs = vi.mocked(fs);
const mockProvider = createMockProvider({
config: {
tools: [
{
functionDeclarations: [
{
name: 'getCurrentTemperature',
parameters: {
type: 'OBJECT',
properties: {
location: { type: 'STRING' },
unit: { type: 'STRING', enum: ['Celsius', 'Fahrenheit'] },
},
required: ['location', 'unit'],
},
},
{
name: 'addOne',
},
],
},
{
googleSearch: {},
},
],
},
response: { output: '' },
}) as ApiProvider;
describe('Google assertions', () => {
beforeEach(() => {
vi.resetAllMocks();
mocks.mockPathResolve.mockImplementation((...args: string[]) => args[args.length - 1]);
mockedFs.existsSync.mockReturnValue(true);
});
describe('API agnostic handleIsValidFunctionCall assertions', () => {
it('should pass when vertex/ais function call matches schema', () => {
const functionOutput = [
{ text: 'test text' },
{
functionCall: {
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA", "unit": "Fahrenheit"}',
},
},
];
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).not.toThrow();
});
it('should pass when Live function call matches schema', () => {
const functionOutput = {
toolCall: {
functionCalls: [
{
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA", "unit": "Fahrenheit"}',
},
],
},
};
expect(() => {
validateFunctionCall(JSON.stringify(functionOutput), mockProvider.config.tools, {});
}).not.toThrow();
});
it('should pass when matches schema no args', () => {
const functionOutput = [
{
functionCall: {
name: 'addOne',
args: '{}',
},
},
];
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).not.toThrow();
});
it('should fail when doesnt match schema parameters', () => {
const functionOutput = [
{
functionCall: {
name: 'addOne',
args: '{"number": 1}',
},
},
];
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).toThrow(
'Call to "addOne":\n{"name":"addOne","args":"{\\"number\\": 1}"}\ndoes not match schema:\n{"name":"addOne"}',
);
});
it('should fail when matches schema no args', () => {
const functionOutput = [
{
functionCall: {
name: 'getCurrentTemperature',
args: '{}',
},
},
];
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).toThrow(
'Call to "getCurrentTemperature":\n{"name":"getCurrentTemperature","args":"{}"}\ndoes not match schema:\n{"name":"getCurrentTemperature","parameters":{"type":"OBJECT","properties":{"location":{"type":"STRING"},"unit":{"type":"STRING","enum":["Celsius","Fahrenheit"]}},"required":["location","unit"]}}',
);
});
it('should load functions from external file', () => {
const functionOutput = [
{
functionCall: {
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA", "unit": "Fahrenheit"}',
},
},
];
const mockYamlContent = `
[
{
"functionDeclarations": [
{
"name": "getCurrentTemperature",
"parameters": {
"type": "OBJECT",
"properties": {
"location": {
"type": "STRING"
},
"unit": {
"type": "STRING",
"enum": ["Celsius", "Fahrenheit"]
}
},
"required": ["location", "unit"]
}
}
]
}
]`;
mockedFs.readFileSync.mockReturnValue(mockYamlContent);
const fileProvider = {
...mockProvider,
config: {
tools: 'file://./test/fixtures/weather_functions.json',
},
};
expect(() => {
validateFunctionCall(functionOutput, fileProvider.config.tools, {});
}).not.toThrow();
// Note: existsSync is no longer called - we use try/catch on readFileSync instead (TOCTOU fix)
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
'./test/fixtures/weather_functions.json',
'utf8',
);
});
it('should render variables in function definitions', () => {
const functionOutput = [
{
functionCall: {
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA", "unit": "custom_unit"}',
},
},
];
const varProvider = {
...mockProvider,
config: {
tools: [
{
functionDeclarations: [
{
name: 'getCurrentTemperature',
parameters: {
type: 'OBJECT',
properties: {
location: { type: 'STRING' },
unit: { type: 'STRING', enum: ['{{unit}}'] },
},
required: ['location', 'unit'],
},
},
],
},
],
},
};
expect(() => {
validateFunctionCall(functionOutput, varProvider.config.tools as Tool[], {
unit: 'custom_unit',
});
}).not.toThrow();
});
it('should fail when functions are not defined', () => {
const functionOutput = [
{
functionCall: {
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA"}',
},
},
];
const emptyProvider = {
...mockProvider,
config: {
tools: [],
},
};
expect(() => {
validateFunctionCall(functionOutput, emptyProvider.config.tools, {});
}).toThrow('Called "getCurrentTemperature", but there is no function with that name');
});
it('should fail when function output is not an object', () => {
const functionOutput = 'not an object';
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).toThrow('Google did not return a valid-looking function call');
});
it('should fail when function call does not match schema', () => {
const functionOutput = [
{
functionCall: {
name: 'getCurrentTemperature',
args: '{"location": "San Francisco, CA"}', // missing required 'unit'
},
},
];
expect(() => {
validateFunctionCall(functionOutput, mockProvider.config.tools, {});
}).toThrow(
'Call to "getCurrentTemperature":\n{"name":"getCurrentTemperature","args":"{\\"location\\": \\"San Francisco, CA\\"}"}\ndoes not match schema:\n[{"instancePath":"","schemaPath":"#/required","keyword":"required","params":{"missingProperty":"unit"},"message":"must have required property \'unit\'"}]',
);
});
});
describe('Unified GoogleProvider api is-valid-function-call assertion', () => {
it('should pass for a direct GoogleProvider instance', async () => {
const output = [{ functionCall: { args: '{"x": 10, "y": 20}', name: 'add' } }];
const provider = new GoogleProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
],
},
});
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider,
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse: { output },
});
expect(result).toMatchObject({
pass: true,
reason: 'Assertion passed',
});
});
});
describe('AI Studio api is-valid-function-call assertion', () => {
it('should pass for a valid function call with correct arguments', async () => {
const output = [{ functionCall: { args: '{"x": 10, "y": 20}', name: 'add' } }];
const provider = new AIStudioChatProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
description: 'add numbers',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
{
googleSearch: {},
},
],
},
});
const providerResponse = { output };
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider,
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse,
});
expect(result).toMatchObject({
pass: true,
reason: 'Assertion passed',
});
});
it('should fail for an invalid function call with incorrect arguments', async () => {
const output = [
{
functionCall: { args: '{"x": "10", "y": 20}', name: 'add' },
},
];
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider: new AIStudioChatProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
],
},
}),
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse: { output },
});
expect(result).toMatchObject({
pass: false,
reason: expect.stringContaining('Call to "add":'),
});
});
});
describe('Vertex api is-valid-function-call assertion', () => {
it('should pass for a valid function call with correct arguments', async () => {
const output = [{ functionCall: { args: '{"x": 10, "y": 20}', name: 'add' } }];
const provider = new VertexChatProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
description: 'add numbers',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
{
googleSearch: {},
},
],
},
});
const providerResponse = { output };
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider,
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse,
});
expect(result).toMatchObject({
pass: true,
reason: 'Assertion passed',
});
});
it('should fail for an invalid function call with incorrect arguments', async () => {
const output = [
{
functionCall: { args: '{"x": "10", "y": 20}', name: 'add' },
},
];
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider: new VertexChatProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
],
},
}),
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse: { output },
});
expect(result).toMatchObject({
pass: false,
reason: expect.stringContaining('Call to "add":'),
});
});
});
describe('Live api is-valid-function-call assertion', () => {
it('should pass for a valid function call with correct arguments', async () => {
const output = JSON.stringify({
toolCall: { functionCalls: [{ args: { x: 10, y: 20 }, name: 'add' }] },
});
const provider = new GoogleLiveProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
description: 'add numbers',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
{
googleSearch: {},
},
],
},
});
const providerResponse = { output };
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider,
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse,
});
expect(result).toMatchObject({
pass: true,
reason: 'Assertion passed',
});
});
it('should fail for an invalid function call with incorrect arguments', async () => {
const output = JSON.stringify({
toolCall: { functionCalls: [{ args: '{"x": "10", "y": 20}', name: 'add' }] },
});
const result: GradingResult = await runAssertion({
prompt: 'Some prompt',
provider: new GoogleLiveProvider('foo', {
config: {
tools: [
{
functionDeclarations: [
{
name: 'add',
parameters: {
type: 'OBJECT',
properties: {
x: { type: 'NUMBER' },
y: { type: 'NUMBER' },
},
required: ['x', 'y'],
},
},
],
},
],
},
}),
assertion: {
type: 'is-valid-function-call',
},
test: {} as AtomicTestCase,
providerResponse: { output },
});
expect(result).toMatchObject({
pass: false,
reason: expect.stringContaining('Call to "add":'),
});
});
});
});