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

1090 lines
35 KiB
TypeScript

import * as fs from 'fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
GoogleVideoProvider,
generateVideoCacheKey,
validateAspectRatio,
validateDuration,
validateResolution,
} from '../../../src/providers/google/video';
import { mockProcessEnv } from '../../util/utils';
import type { CallApiContextParams } from '../../../src/types/providers';
// Mock the Google client
const mockRequest = vi.fn();
const mockFetchWithTimeout = vi.fn();
const mockGetGoogleClient = vi.fn().mockResolvedValue({
client: { request: mockRequest },
projectId: 'test-project',
});
// Mock blob storage
const mockStoreBlob = vi.fn();
vi.mock('../../../src/blobs', () => ({
storeBlob: (...args: unknown[]) => mockStoreBlob(...args),
}));
vi.mock('fs');
const mockResolveProjectId = vi.fn().mockResolvedValue('test-project');
const mockGetGoogleApiKey = vi.fn();
const mockDetermineGoogleVertexMode = vi.fn();
vi.mock('../../../src/providers/google/util', () => ({
getGoogleClient: () => mockGetGoogleClient(),
loadCredentials: vi.fn((creds) => creds),
resolveProjectId: (...args: unknown[]) => mockResolveProjectId(...args),
getGoogleApiKey: (...args: unknown[]) => mockGetGoogleApiKey(...args),
determineGoogleVertexMode: (...args: unknown[]) => mockDetermineGoogleVertexMode(...args),
}));
vi.mock('../../../src/util/fetch/index', () => ({
fetchWithTimeout: (...args: unknown[]) => mockFetchWithTimeout(...args),
}));
vi.mock('../../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
describe('GoogleVideoProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
mockRequest.mockReset();
mockFetchWithTimeout.mockReset();
mockStoreBlob.mockReset();
mockResolveProjectId.mockReset();
mockGetGoogleApiKey.mockReset();
mockDetermineGoogleVertexMode.mockReset();
mockResolveProjectId.mockResolvedValue('test-project');
mockGetGoogleApiKey.mockImplementation((config: any, env?: any) => ({
apiKey:
config?.apiKey ||
env?.GOOGLE_API_KEY ||
process.env.GOOGLE_API_KEY ||
env?.GEMINI_API_KEY ||
process.env.GEMINI_API_KEY,
source: 'GOOGLE_API_KEY',
}));
mockDetermineGoogleVertexMode.mockImplementation((config: any, env?: any) => {
if (config?.vertexai !== undefined) {
return config.vertexai;
}
return Boolean(
config?.projectId ||
config?.credentials ||
env?.GOOGLE_CLOUD_PROJECT ||
env?.GOOGLE_PROJECT_ID ||
process.env.GOOGLE_CLOUD_PROJECT ||
process.env.GOOGLE_PROJECT_ID,
);
});
mockProcessEnv({ GOOGLE_PROJECT_ID: 'test-project' });
mockProcessEnv({ GOOGLE_CLOUD_PROJECT: undefined });
mockProcessEnv({ GOOGLE_API_KEY: undefined });
mockProcessEnv({ GEMINI_API_KEY: undefined });
mockProcessEnv({ VERTEX_PROJECT_ID: undefined });
// Default mock for blob storage
mockStoreBlob.mockResolvedValue({
ref: {
uri: 'promptfoo://blob/abc123def456',
hash: 'abc123def456',
mimeType: 'video/mp4',
sizeBytes: 1024,
provider: 'filesystem',
},
deduplicated: false,
});
});
afterEach(() => {
mockProcessEnv({ GOOGLE_API_KEY: undefined });
mockProcessEnv({ GEMINI_API_KEY: undefined });
mockProcessEnv({ GOOGLE_CLOUD_PROJECT: undefined });
mockProcessEnv({ GOOGLE_PROJECT_ID: undefined });
mockProcessEnv({ VERTEX_PROJECT_ID: undefined });
// Reset fs mocks to prevent leakage between tests
vi.mocked(fs.existsSync).mockReset();
vi.mocked(fs.readFileSync).mockReset();
});
describe('constructor and id', () => {
it('should construct with model name', () => {
const provider = new GoogleVideoProvider('veo-3.1-generate-preview');
expect(provider.id()).toBe('google:video:veo-3.1-generate-preview');
expect(provider.toString()).toBe('[Google Video Provider veo-3.1-generate-preview]');
});
it('should support custom provider ID', () => {
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
id: 'my-custom-id',
});
expect(provider.id()).toBe('my-custom-id');
});
});
describe('validateAspectRatio', () => {
it('should accept 16:9', () => {
expect(validateAspectRatio('16:9')).toEqual({ valid: true });
});
it('should accept 9:16', () => {
expect(validateAspectRatio('9:16')).toEqual({ valid: true });
});
it('should reject invalid ratio', () => {
const result = validateAspectRatio('4:3');
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid aspect ratio');
});
});
describe('validateDuration', () => {
it('should accept 4, 6, 8 for Veo 3.1', () => {
expect(validateDuration('veo-3.1-generate-preview', 4)).toEqual({ valid: true });
expect(validateDuration('veo-3.1-generate-preview', 6)).toEqual({ valid: true });
expect(validateDuration('veo-3.1-generate-preview', 8)).toEqual({ valid: true });
});
it('should accept 4, 6, 8 for Veo 3', () => {
expect(validateDuration('veo-3-generate', 4)).toEqual({ valid: true });
expect(validateDuration('veo-3-generate', 6)).toEqual({ valid: true });
expect(validateDuration('veo-3-generate', 8)).toEqual({ valid: true });
});
it('should accept 5, 6, 8 for Veo 2', () => {
expect(validateDuration('veo-2-generate', 5)).toEqual({ valid: true });
expect(validateDuration('veo-2-generate', 6)).toEqual({ valid: true });
expect(validateDuration('veo-2-generate', 8)).toEqual({ valid: true });
});
it('should reject 5 for Veo 3', () => {
const result = validateDuration('veo-3.1-generate-preview', 5);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid duration');
});
it('should reject 4 for Veo 2', () => {
const result = validateDuration('veo-2-generate', 4);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid duration');
});
});
describe('validateResolution', () => {
it('should accept 720p and 1080p for Veo 3.1 16:9', () => {
expect(validateResolution('veo-3.1-generate-preview', '16:9', '720p')).toEqual({
valid: true,
});
expect(validateResolution('veo-3.1-generate-preview', '16:9', '1080p')).toEqual({
valid: true,
});
});
it('should reject 1080p for Veo 3 with 9:16 aspect ratio', () => {
const result = validateResolution('veo-3-generate', '9:16', '1080p');
expect(result.valid).toBe(false);
expect(result.message).toContain('Veo 3 only supports 1080p for 16:9');
});
it('should reject 1080p for Veo 2', () => {
const result = validateResolution('veo-2-generate', '16:9', '1080p');
expect(result.valid).toBe(false);
expect(result.message).toContain('Veo 2 only supports 720p');
});
});
describe('generateVideoCacheKey', () => {
it('should generate deterministic key', () => {
const key1 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
const key2 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
expect(key1).toBe(key2);
});
it('should generate different keys for different prompts', () => {
const key1 = generateVideoCacheKey('prompt1', 'veo-3.1', '16:9', '720p', 8);
const key2 = generateVideoCacheKey('prompt2', 'veo-3.1', '16:9', '720p', 8);
expect(key1).not.toBe(key2);
});
it('should generate different keys for different models', () => {
const key1 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
const key2 = generateVideoCacheKey('prompt', 'veo-2', '16:9', '720p', 8);
expect(key1).not.toBe(key2);
});
it('should generate different keys for different durations', () => {
const key1 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 4);
const key2 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
expect(key1).not.toBe(key2);
});
it('should include image data in cache key', () => {
const key1 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8, 'imageBase64');
const key2 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
expect(key1).not.toBe(key2);
});
it('should include negative prompt in cache key', () => {
const key1 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8, undefined, 'blur');
const key2 = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
expect(key1).not.toBe(key2);
});
it('should return UUID-like format', () => {
const key = generateVideoCacheKey('prompt', 'veo-3.1', '16:9', '720p', 8);
expect(key).toMatch(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/);
});
});
describe('callApi', () => {
it('should return error when Google AI Studio API key is missing', async () => {
mockProcessEnv({ GOOGLE_CLOUD_PROJECT: undefined });
mockProcessEnv({ GOOGLE_PROJECT_ID: undefined });
mockResolveProjectId.mockRejectedValue(new Error('No project ID found'));
const provider = new GoogleVideoProvider('veo-3.1-generate-preview');
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Google AI Studio');
expect(result.error).toContain('GOOGLE_API_KEY');
});
it('should return error when Vertex project ID is missing and ADC fails', async () => {
mockProcessEnv({ GOOGLE_CLOUD_PROJECT: undefined });
mockProcessEnv({ GOOGLE_PROJECT_ID: undefined });
mockResolveProjectId.mockRejectedValue(new Error('No project ID found'));
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
vertexai: true,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Vertex AI');
expect(result.error).toContain('GOOGLE_CLOUD_PROJECT');
});
it('should resolve project ID from ADC when not explicitly set', async () => {
mockProcessEnv({ GOOGLE_CLOUD_PROJECT: undefined });
mockProcessEnv({ GOOGLE_PROJECT_ID: undefined });
// ADC can resolve the project ID
mockResolveProjectId.mockResolvedValue('adc-resolved-project');
const operationName =
'projects/adc-resolved-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: { name: operationName, done: false },
});
// Mock polling - done with video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: { videos: [{ bytesBase64Encoded: base64Video }] },
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview');
const result = await provider.callApi('Test prompt');
expect(result.error).toBeUndefined();
expect(mockResolveProjectId).toHaveBeenCalled();
});
it('should return error when prompt is empty', async () => {
const provider = new GoogleVideoProvider('veo-3.1-generate-preview');
const result = await provider.callApi('');
expect(result.error).toBe('Prompt is required for video generation');
});
it('should return error for invalid aspect ratio', async () => {
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
aspectRatio: '4:3' as any,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Invalid aspect ratio');
});
it('should return error for invalid duration', async () => {
const provider = new GoogleVideoProvider('veo-2-generate', {
config: {
durationSeconds: 4,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Invalid duration');
});
it('should create video job, poll for completion, and store to blob storage', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
// Base64 encoded MP4 header (simplified for test)
const base64Video = Buffer.from('fake mp4 video data').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - not done yet
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
metadata: { progress: 50 },
},
});
// Mock polling - done with base64 video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
'@type': 'type.googleapis.com/cloud.ai.large_models.vision.GenerateVideoResponse',
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
maxPollTimeMs: 5000,
},
});
const result = await provider.callApi('A cat playing piano', {
evaluationId: 'eval-google-video',
promptIdx: 4,
testIdx: 3,
} as unknown as CallApiContextParams);
expect(result.error).toBeUndefined();
expect(result.cached).toBe(false);
expect(result.output).toContain('[Video:');
expect(result.output).toContain('promptfoo://blob/');
expect(result.video).toBeDefined();
expect(result.video?.format).toBe('mp4');
expect(result.video?.model).toBe('veo-3.1-generate-preview');
expect(result.video?.blobRef).toBeDefined();
expect(result.video?.blobRef?.uri).toContain('promptfoo://blob/');
expect(result.video?.url).toBe(result.video?.blobRef?.uri); // url matches blobRef.uri
// 3 requests: job creation, 2 polls (one in progress, one done)
expect(mockRequest).toHaveBeenCalledTimes(3);
// Blob storage called once
expect(mockStoreBlob).toHaveBeenCalledTimes(1);
expect(mockStoreBlob).toHaveBeenCalledWith(
expect.any(Buffer),
'video/mp4',
expect.objectContaining({
evalId: 'eval-google-video',
kind: 'video',
promptIdx: 4,
testIdx: 3,
}),
);
});
it('should create and poll Veo jobs through Google AI Studio with an API key', async () => {
mockProcessEnv({ GOOGLE_PROJECT_ID: undefined });
mockProcessEnv({ GOOGLE_API_KEY: 'test-api-key' });
const operationName = 'models/veo-3.1-generate-preview/operations/test-op';
const videoUri = 'https://generativelanguage.googleapis.com/v1beta/files/test-video';
const videoBytes = Buffer.from('fake ai studio video data');
mockFetchWithTimeout
.mockResolvedValueOnce(
new Response(JSON.stringify({ name: operationName, done: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
name: operationName,
done: true,
response: {
generateVideoResponse: {
generatedSamples: [{ video: { uri: videoUri } }],
},
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
},
),
)
.mockResolvedValueOnce(
new Response(videoBytes, {
status: 200,
headers: { 'Content-Type': 'video/mp4' },
}),
);
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
maxPollTimeMs: 5000,
},
});
const result = await provider.callApi('A cinematic shot of a lighthouse in a storm', {
evaluationId: 'eval-google-video-download',
promptIdx: 9,
testIdx: 10,
} as unknown as CallApiContextParams);
expect(result.error).toBeUndefined();
expect(result.video?.model).toBe('veo-3.1-generate-preview');
expect(result.video?.blobRef?.uri).toContain('promptfoo://blob/');
expect(mockStoreBlob).toHaveBeenCalledWith(
expect.any(Buffer),
'video/mp4',
expect.objectContaining({
evalId: 'eval-google-video-download',
kind: 'video',
promptIdx: 9,
testIdx: 10,
}),
);
expect(mockResolveProjectId).not.toHaveBeenCalled();
expect(mockFetchWithTimeout).toHaveBeenCalledTimes(3);
expect(mockFetchWithTimeout).toHaveBeenNthCalledWith(
1,
'https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'x-goog-api-key': 'test-api-key',
}),
}),
expect.any(Number),
);
expect(mockFetchWithTimeout.mock.calls[0]?.[1]?.body).toContain(
'"prompt":"A cinematic shot of a lighthouse in a storm"',
);
expect(mockFetchWithTimeout.mock.calls[0]?.[1]?.body).toContain('"durationSeconds":8');
expect(mockFetchWithTimeout).toHaveBeenNthCalledWith(
2,
'https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview/operations/test-op',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'x-goog-api-key': 'test-api-key',
}),
}),
expect.any(Number),
);
expect(mockFetchWithTimeout).toHaveBeenNthCalledWith(
3,
videoUri,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'x-goog-api-key': 'test-api-key',
}),
}),
expect.any(Number),
);
});
it('should handle API error on job creation', async () => {
mockRequest.mockRejectedValueOnce({
response: {
data: {
error: { message: 'Invalid prompt' },
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview');
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Failed to create video job');
expect(result.error).toContain('Invalid prompt');
});
it('should handle polling timeout', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - always not done
mockRequest.mockResolvedValue({
data: {
name: operationName,
done: false,
metadata: { progress: 10 },
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
maxPollTimeMs: 50, // Very short timeout
},
});
vi.useFakeTimers();
try {
const resultPromise = provider.callApi('Test prompt');
await vi.runAllTimersAsync();
const result = await resultPromise;
expect(result.error).toContain('timed out');
} finally {
vi.useRealTimers();
}
});
it('should handle video generation error', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - error
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
error: {
code: 400,
message: 'Content policy violation',
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Video generation failed');
expect(result.error).toContain('Content policy violation');
});
it('should handle download error for legacy URI format', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const videoUri = 'https://storage.googleapis.com/video.mp4';
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done with legacy URI format
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
generateVideoResponse: {
generatedSamples: [{ video: { uri: videoUri } }],
},
},
},
});
// Mock video download - error
mockRequest.mockRejectedValueOnce({
message: 'Network error',
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Download error');
});
it('should include config options in API request', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
aspectRatio: '9:16',
resolution: '720p',
durationSeconds: 4,
negativePrompt: 'blur, noise',
personGeneration: 'dont_allow',
seed: 12345,
pollIntervalMs: 10,
},
});
await provider.callApi('Test prompt');
// First call is job creation
expect(mockRequest).toHaveBeenCalled();
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].aspectRatio).toBe('9:16');
expect(body.instances[0].resolution).toBe('720p');
expect(body.instances[0].durationSeconds).toBe('4');
expect(body.instances[0].negativePrompt).toBe('blur, noise');
expect(body.instances[0].personGeneration).toBe('dont_allow');
expect(body.instances[0].seed).toBe(12345);
});
it('should include video extension in request', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation (POST)
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling (POST) - returns done with video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
extendVideoId: 'previous-operation-id',
pollIntervalMs: 10,
},
});
const result = await provider.callApi('Continue the video');
expect(mockRequest).toHaveBeenCalled();
expect(result.error).toBeUndefined();
expect(result.cached).toBe(false);
// Should include video extension in request
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].video).toEqual({ operationName: 'previous-operation-id' });
});
it('should handle blob storage deduplication', async () => {
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock deduplicated blob storage response
mockStoreBlob.mockResolvedValueOnce({
ref: {
uri: 'promptfoo://blob/existinghash123',
hash: 'existinghash123',
mimeType: 'video/mp4',
sizeBytes: 1024,
provider: 'filesystem',
},
deduplicated: true,
});
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
pollIntervalMs: 10,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toBeUndefined();
expect(result.video?.blobRef?.hash).toBe('existinghash123');
});
});
describe('image-to-video', () => {
it('should include image data in request', async () => {
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === '/path/to/image.png') {
return true;
}
return false;
});
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('fake-image-data'));
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
image: 'file:///path/to/image.png',
pollIntervalMs: 10,
},
});
await provider.callApi('Animate this image');
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].image).toEqual({
imageBytes: 'ZmFrZS1pbWFnZS1kYXRh', // base64 of 'fake-image-data'
mimeType: 'image/png',
});
});
it('should return error for missing image file', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
image: 'file:///path/to/missing.png',
},
});
const result = await provider.callApi('Animate this image');
expect(result.error).toContain('Image file not found');
});
});
describe('reference images (Veo 3.1)', () => {
it('should include reference images in request', async () => {
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === '/path/to/ref1.png' || path === '/path/to/ref2.png') {
return true;
}
return false;
});
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('ref-image-data'));
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done with base64 video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
referenceImages: [
{ image: 'file:///path/to/ref1.png', referenceType: 'asset' },
{ image: 'file:///path/to/ref2.png', referenceType: 'asset' },
],
pollIntervalMs: 10,
},
});
await provider.callApi('Generate with references');
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].referenceImages).toHaveLength(2);
expect(body.instances[0].referenceImages[0].referenceType).toBe('asset');
});
it('should limit reference images to 3', async () => {
vi.mocked(fs.existsSync).mockReset();
// Return true only for the reference image file paths
vi.mocked(fs.existsSync).mockImplementation((path) => {
const pathStr = path.toString();
if (pathStr.includes('/path/to/ref')) {
return true; // Reference image files exist
}
return false;
});
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('ref-image-data'));
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done with base64 video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
referenceImages: [
{ image: 'file:///path/to/ref1.png', referenceType: 'asset' },
{ image: 'file:///path/to/ref2.png', referenceType: 'asset' },
{ image: 'file:///path/to/ref3.png', referenceType: 'asset' },
{ image: 'file:///path/to/ref4.png', referenceType: 'asset' }, // Should be ignored
],
pollIntervalMs: 10,
},
});
await provider.callApi('Generate with references');
expect(mockRequest).toHaveBeenCalled();
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].referenceImages).toHaveLength(3);
});
});
describe('interpolation (Veo 3.1)', () => {
it('should include last frame in request', async () => {
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === '/path/to/first.png' || path === '/path/to/last.png') {
return true;
}
return false;
});
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('frame-data'));
const operationName =
'projects/test-project/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/test-op';
const base64Video = Buffer.from('fake video').toString('base64');
// Mock job creation
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: false,
},
});
// Mock polling - done with base64 video
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: {
videos: [{ bytesBase64Encoded: base64Video }],
},
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
image: 'file:///path/to/first.png',
lastFrame: 'file:///path/to/last.png',
pollIntervalMs: 10,
},
});
await provider.callApi('Interpolate between frames');
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].image).toBeDefined();
expect(body.instances[0].lastFrame).toBeDefined();
});
it('should support lastImage alias for interpolation', async () => {
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === '/path/to/first.png' || path === '/path/to/last.png') {
return true;
}
return false;
});
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('frame-data'));
const operationName = 'test-op';
const base64Video = Buffer.from('fake video').toString('base64');
mockRequest.mockResolvedValueOnce({
data: { name: operationName, done: false },
});
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: { videos: [{ bytesBase64Encoded: base64Video }] },
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
image: 'file:///path/to/first.png',
lastImage: 'file:///path/to/last.png',
pollIntervalMs: 10,
},
});
await provider.callApi('Interpolate');
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].lastFrame).toBeDefined();
});
it('should support sourceVideo alias for extension', async () => {
const operationName = 'test-op';
const base64Video = Buffer.from('fake video').toString('base64');
mockRequest.mockResolvedValueOnce({
data: { name: operationName, done: false },
});
mockRequest.mockResolvedValueOnce({
data: {
name: operationName,
done: true,
response: { videos: [{ bytesBase64Encoded: base64Video }] },
},
});
const provider = new GoogleVideoProvider('veo-3.1-generate-preview', {
config: {
sourceVideo: 'previous-op-id',
pollIntervalMs: 10,
},
});
await provider.callApi('Extend');
const firstCallOptions = mockRequest.mock.calls[0][0];
const body = JSON.parse(firstCallOptions.body);
expect(body.instances[0].video).toEqual({ operationName: 'previous-op-id' });
});
});
});