Files
promptfoo--promptfoo/test/providers/azure/video.test.ts
T
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

612 lines
17 KiB
TypeScript

import fsPromises from 'fs/promises';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
AZURE_SORA_COST_PER_SECOND,
AZURE_VIDEO_DIMENSIONS,
AZURE_VIDEO_DURATIONS,
} from '../../../src/providers/azure/defaults';
import {
AzureVideoProvider,
calculateAzureVideoCost,
validateAzureVideoDimensions,
validateAzureVideoDuration,
} from '../../../src/providers/azure/video';
import { generateVideoCacheKey } from '../../../src/providers/video';
// Hoist mock functions so they're available in vi.mock factories
const {
mockFetchWithProxy,
mockStoreMedia,
mockMediaExists,
mockGetMediaStorage,
mockGetConfigDirectoryPath,
} = vi.hoisted(() => ({
mockFetchWithProxy: vi.fn(),
mockStoreMedia: vi.fn(),
mockMediaExists: vi.fn(),
mockGetMediaStorage: vi.fn(),
mockGetConfigDirectoryPath: vi.fn(),
}));
const fsPromiseMocks = vi.hoisted(() => ({
mkdir: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
}));
// Mock the dependencies
vi.mock('fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
...actual,
default: {
...actual,
...fsPromiseMocks,
},
...fsPromiseMocks,
};
});
vi.mock('../../../src/storage', () => ({
storeMedia: mockStoreMedia,
mediaExists: mockMediaExists,
getMediaStorage: mockGetMediaStorage,
}));
vi.mock('../../../src/util/config/manage', () => ({
getConfigDirectoryPath: mockGetConfigDirectoryPath,
}));
vi.mock('../../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
logRequestResponse: vi.fn(),
}));
vi.mock('../../../src/util/fetch/index', () => ({
fetchWithProxy: mockFetchWithProxy,
}));
describe('AzureVideoProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
mockFetchWithProxy.mockReset();
mockStoreMedia.mockReset();
mockMediaExists.mockReset();
mockGetMediaStorage.mockReset();
mockGetConfigDirectoryPath.mockReset();
// Default config directory path
mockGetConfigDirectoryPath.mockReturnValue('/test/config');
vi.mocked(fsPromises.readFile).mockRejectedValue(
Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' }),
);
vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined);
vi.mocked(fsPromises.writeFile).mockResolvedValue(undefined);
// Default: no cached video exists
const mockStorage = {
exists: vi.fn().mockResolvedValue(false),
};
mockGetMediaStorage.mockReturnValue(mockStorage);
// Default store response
mockStoreMedia.mockResolvedValue({
ref: {
provider: 'local',
key: 'video/abc123.mp4',
contentHash: 'abc123',
metadata: {
contentType: 'video/mp4',
mediaType: 'video',
},
},
deduplicated: false,
});
});
afterEach(() => {
vi.resetAllMocks();
});
describe('validateAzureVideoDimensions', () => {
it('should accept valid 1280x720 dimensions', () => {
expect(validateAzureVideoDimensions(1280, 720)).toEqual({ valid: true });
});
it('should accept valid 1920x1080 dimensions', () => {
expect(validateAzureVideoDimensions(1920, 1080)).toEqual({ valid: true });
});
it('should accept valid square dimensions', () => {
expect(validateAzureVideoDimensions(720, 720)).toEqual({ valid: true });
expect(validateAzureVideoDimensions(1080, 1080)).toEqual({ valid: true });
});
it('should accept valid 854x480 dimensions', () => {
expect(validateAzureVideoDimensions(854, 480)).toEqual({ valid: true });
});
it('should reject invalid dimensions', () => {
const result = validateAzureVideoDimensions(1920, 720);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid video dimensions');
expect(result.message).toContain('1920x720');
});
it('should list valid sizes in error message', () => {
const result = validateAzureVideoDimensions(999, 999);
expect(result.message).toContain('480x480');
expect(result.message).toContain('1280x720');
});
});
describe('validateAzureVideoDuration', () => {
it('should accept valid durations', () => {
for (const duration of AZURE_VIDEO_DURATIONS) {
expect(validateAzureVideoDuration(duration)).toEqual({ valid: true });
}
});
it('should reject invalid duration', () => {
// TypeScript requires cast for invalid values
const result = validateAzureVideoDuration(8 as 5);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid video duration');
});
});
describe('calculateAzureVideoCost', () => {
it('should calculate cost correctly for 5 seconds', () => {
const cost = calculateAzureVideoCost(5, false);
expect(cost).toBe(AZURE_SORA_COST_PER_SECOND * 5);
});
it('should calculate cost correctly for 20 seconds', () => {
const cost = calculateAzureVideoCost(20, false);
expect(cost).toBe(AZURE_SORA_COST_PER_SECOND * 20);
});
it('should return 0 for cached videos', () => {
const cost = calculateAzureVideoCost(10, true);
expect(cost).toBe(0);
});
});
describe('constructor', () => {
it('should create provider with deployment name', () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.cognitiveservices.azure.com',
apiKey: 'test-key',
},
});
expect(provider.id()).toBe('azure:video:sora');
});
it('should use custom provider ID if specified', () => {
const provider = new AzureVideoProvider('sora', {
id: 'my-custom-video-provider',
config: {
apiBaseUrl: 'https://test.cognitiveservices.azure.com',
apiKey: 'test-key',
},
});
expect(provider.id()).toBe('my-custom-video-provider');
});
it('should store config options', () => {
const config = {
apiBaseUrl: 'https://custom.azure.com',
apiKey: 'custom-key',
width: 1920,
height: 1080,
n_seconds: 10 as const,
};
const provider = new AzureVideoProvider('sora', { config });
expect(provider.config).toEqual(config);
});
});
describe('toString', () => {
it('should return descriptive string', () => {
const provider = new AzureVideoProvider('my-deployment', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
expect(provider.toString()).toBe('[Azure Video Provider my-deployment]');
});
});
describe('callApi - validation', () => {
it('should reject invalid video dimensions', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
width: 999,
height: 999,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Invalid video dimensions');
});
it('should reject invalid video duration', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
n_seconds: 7 as 5,
},
});
const result = await provider.callApi('Test prompt');
expect(result.error).toContain('Invalid video duration');
});
it('should return error if API base URL is not set', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiKey: 'test-key',
},
});
const result = await provider.callApi('Test prompt');
// The error is returned during job creation since base URL is null
expect(result.error).toBeDefined();
});
});
describe('callApi - successful flow', () => {
it('should create and poll video job successfully', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.cognitiveservices.azure.com',
apiKey: 'test-key',
},
});
// Mock job creation
mockFetchWithProxy
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'queued',
generations: [],
}),
})
// Mock status polling - succeeded
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [
{
id: 'gen_456',
job_id: 'job_123',
width: 1280,
height: 720,
n_seconds: 5,
prompt: 'A cat playing piano',
},
],
}),
})
// Mock video download
.mockResolvedValueOnce({
ok: true,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(1000)),
});
const result = await provider.callApi('A cat playing piano');
expect(result.error).toBeUndefined();
expect(result.output).toContain('[Video:');
expect(result.video).toBeDefined();
expect(result.video?.format).toBe('mp4');
expect(result.video?.size).toBe('1280x720');
expect(result.video?.duration).toBe(5);
expect(result.cached).toBe(false);
});
it('should use custom dimensions and duration', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
width: 1920,
height: 1080,
n_seconds: 10,
},
});
mockFetchWithProxy
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [{ id: 'gen_456', width: 1920, height: 1080, n_seconds: 10 }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [{ id: 'gen_456', width: 1920, height: 1080, n_seconds: 10 }],
}),
})
.mockResolvedValueOnce({
ok: true,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(1000)),
});
const result = await provider.callApi('Test');
expect(result.video?.size).toBe('1920x1080');
expect(result.video?.duration).toBe(10);
});
});
describe('callApi - error handling', () => {
it('should handle job creation failure', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 429,
statusText: 'Too Many Requests',
json: () =>
Promise.resolve({
error: { message: 'Rate limit exceeded' },
}),
});
const result = await provider.callApi('Test');
expect(result.error).toContain('Rate limit exceeded');
});
it('should handle polling failure', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
// Job creation succeeds
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ id: 'job_123', status: 'queued' }),
});
// Status check fails
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'failed',
failure_reason: 'Content policy violation',
}),
});
const result = await provider.callApi('Test');
expect(result.error).toContain('Content policy violation');
});
it('should handle video download failure', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
mockFetchWithProxy
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [{ id: 'gen_456' }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [{ id: 'gen_456' }],
}),
})
.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
});
const result = await provider.callApi('Test');
expect(result.error).toContain('Failed to download video');
});
it('should handle empty generations array', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
mockFetchWithProxy
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [],
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: 'job_123',
status: 'succeeded',
generations: [],
}),
});
const result = await provider.callApi('Test');
expect(result.error).toContain('No video generations returned');
});
});
describe('callApi - caching', () => {
it('should return cached video when available', async () => {
const provider = new AzureVideoProvider('sora', {
config: {
apiBaseUrl: 'https://test.azure.com',
apiKey: 'test-key',
},
});
// Set up cache hit
vi.mocked(fsPromises.readFile).mockResolvedValue(
JSON.stringify({
videoKey: 'video/cached123.mp4',
createdAt: '2024-01-01T00:00:00Z',
}),
);
mockGetMediaStorage.mockReturnValue({
exists: vi.fn().mockResolvedValue(true),
});
const result = await provider.callApi('Test prompt');
expect(result.cached).toBe(true);
expect(result.latencyMs).toBe(0);
expect(result.cost).toBe(0);
expect(result.video?.storageRef?.key).toBe('video/cached123.mp4');
// Should not have made any fetch calls
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
});
describe('generateVideoCacheKey for Azure', () => {
it('should generate deterministic cache keys', () => {
const key1 = generateVideoCacheKey({
provider: 'azure',
prompt: 'A cat playing piano',
model: 'sora',
size: '1280x720',
seconds: 5,
});
const key2 = generateVideoCacheKey({
provider: 'azure',
prompt: 'A cat playing piano',
model: 'sora',
size: '1280x720',
seconds: 5,
});
expect(key1).toBe(key2);
});
it('should generate different keys for different prompts', () => {
const key1 = generateVideoCacheKey({
provider: 'azure',
prompt: 'A cat',
model: 'sora',
size: '1280x720',
seconds: 5,
});
const key2 = generateVideoCacheKey({
provider: 'azure',
prompt: 'A dog',
model: 'sora',
size: '1280x720',
seconds: 5,
});
expect(key1).not.toBe(key2);
});
it('should generate different keys for azure vs openai provider', () => {
const azureKey = generateVideoCacheKey({
provider: 'azure',
prompt: 'Test',
model: 'sora',
size: '1280x720',
seconds: 5,
});
const openaiKey = generateVideoCacheKey({
provider: 'openai',
prompt: 'Test',
model: 'sora',
size: '1280x720',
seconds: 5,
});
expect(azureKey).not.toBe(openaiKey);
});
});
describe('AZURE_VIDEO_DIMENSIONS', () => {
it('should have all required dimensions', () => {
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('480x480');
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('854x480');
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('720x720');
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('1280x720');
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('1080x1080');
expect(Object.keys(AZURE_VIDEO_DIMENSIONS)).toContain('1920x1080');
});
it('should have correct width and height for each dimension', () => {
expect(AZURE_VIDEO_DIMENSIONS['1280x720']).toEqual({ width: 1280, height: 720 });
expect(AZURE_VIDEO_DIMENSIONS['1920x1080']).toEqual({ width: 1920, height: 1080 });
});
});
describe('AZURE_VIDEO_DURATIONS', () => {
it('should include valid durations', () => {
expect(AZURE_VIDEO_DURATIONS).toContain(5);
expect(AZURE_VIDEO_DURATIONS).toContain(10);
expect(AZURE_VIDEO_DURATIONS).toContain(15);
expect(AZURE_VIDEO_DURATIONS).toContain(20);
});
});
});