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
1301 lines
40 KiB
TypeScript
1301 lines
40 KiB
TypeScript
import fsPromises from 'fs/promises';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
calculateVideoCost,
|
|
OpenAiVideoProvider,
|
|
SORA_COSTS,
|
|
validateVideoSeconds,
|
|
validateVideoSize,
|
|
} from '../../../src/providers/openai/video';
|
|
import { checkVideoCache, generateVideoCacheKey } from '../../../src/providers/video';
|
|
import { mockProcessEnv } from '../../util/utils';
|
|
import { getOpenAiMissingApiKeyMessage } from './shared';
|
|
|
|
// 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('OpenAiVideoProvider', () => {
|
|
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('validateVideoSize', () => {
|
|
it('should accept valid landscape size', () => {
|
|
expect(validateVideoSize('1280x720')).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should accept valid widescreen size', () => {
|
|
expect(validateVideoSize('1792x1024')).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should accept valid portrait size', () => {
|
|
expect(validateVideoSize('720x1280')).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should accept valid tall portrait size', () => {
|
|
expect(validateVideoSize('1024x1792')).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should reject invalid size', () => {
|
|
// Cast to test runtime validation with invalid values
|
|
const result = validateVideoSize('1920x1080' as '1280x720');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toContain('Invalid video size');
|
|
expect(result.message).toContain('1920x1080');
|
|
});
|
|
|
|
it('should reject non-standard sizes', () => {
|
|
// Cast to test runtime validation with invalid values
|
|
const result = validateVideoSize('500x500' as '1280x720');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('validateVideoSeconds', () => {
|
|
it('should accept 4 seconds', () => {
|
|
expect(validateVideoSeconds(4)).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should accept 8 seconds', () => {
|
|
expect(validateVideoSeconds(8)).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should accept 12 seconds', () => {
|
|
expect(validateVideoSeconds(12)).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should reject invalid duration like 5 seconds', () => {
|
|
// Cast to test runtime validation with invalid values
|
|
const result = validateVideoSeconds(5 as 4);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toContain('Invalid video duration');
|
|
expect(result.message).toContain('5');
|
|
expect(result.message).toContain('4, 8, 12');
|
|
});
|
|
|
|
it('should reject 10 seconds', () => {
|
|
// Cast to test runtime validation with invalid values
|
|
const result = validateVideoSeconds(10 as 4);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject 0 seconds', () => {
|
|
// Cast to test runtime validation with invalid values
|
|
const result = validateVideoSeconds(0 as 4);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('calculateVideoCost', () => {
|
|
it('should calculate cost for sora-2', () => {
|
|
// $0.10 per second * 10 seconds = $1.00
|
|
expect(calculateVideoCost('sora-2', 10)).toBe(1.0);
|
|
});
|
|
|
|
it('should calculate cost for sora-2-pro', () => {
|
|
// $0.30 per second * 10 seconds = $3.00
|
|
expect(calculateVideoCost('sora-2-pro', 10)).toBe(3.0);
|
|
});
|
|
|
|
it('should return 0 for cached results', () => {
|
|
expect(calculateVideoCost('sora-2', 10, true)).toBe(0);
|
|
expect(calculateVideoCost('sora-2-pro', 10, true)).toBe(0);
|
|
});
|
|
|
|
it('should calculate cost for different durations', () => {
|
|
expect(calculateVideoCost('sora-2', 5)).toBe(0.5);
|
|
expect(calculateVideoCost('sora-2', 20)).toBe(2.0);
|
|
});
|
|
});
|
|
|
|
describe('SORA_COSTS', () => {
|
|
it('should have correct cost for sora-2', () => {
|
|
expect(SORA_COSTS['sora-2']).toBe(0.1);
|
|
});
|
|
|
|
it('should have correct cost for sora-2-pro', () => {
|
|
expect(SORA_COSTS['sora-2-pro']).toBe(0.3);
|
|
});
|
|
});
|
|
|
|
describe('provider id', () => {
|
|
it('should return correct provider ID', () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
expect(provider.id()).toBe('openai:video:sora-2');
|
|
});
|
|
|
|
it('should use custom ID if provided', () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
id: 'custom-video-provider',
|
|
});
|
|
expect(provider.id()).toBe('custom-video-provider');
|
|
});
|
|
});
|
|
|
|
describe('toString', () => {
|
|
it('should return correct string representation', () => {
|
|
const provider = new OpenAiVideoProvider('sora-2-pro', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
expect(provider.toString()).toBe('[OpenAI Video Provider sora-2-pro]');
|
|
});
|
|
});
|
|
|
|
describe('callApi', () => {
|
|
const setupMocksForSuccess = () => {
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued', progress: 0 }),
|
|
});
|
|
|
|
// Mock polling - completed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'completed', progress: 100 }),
|
|
});
|
|
|
|
// Mock video download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
|
|
// Mock thumbnail download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
|
|
// Mock spritesheet download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage for each asset
|
|
mockStoreMedia
|
|
.mockResolvedValueOnce({
|
|
ref: {
|
|
provider: 'local',
|
|
key: 'video/abc123.mp4',
|
|
contentHash: 'abc123',
|
|
metadata: { contentType: 'video/mp4', mediaType: 'video' },
|
|
},
|
|
deduplicated: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ref: {
|
|
provider: 'local',
|
|
key: 'video/abc123.webp',
|
|
contentHash: 'abc123',
|
|
metadata: { contentType: 'image/webp', mediaType: 'image' },
|
|
},
|
|
deduplicated: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ref: {
|
|
provider: 'local',
|
|
key: 'video/abc123.jpg',
|
|
contentHash: 'abc123',
|
|
metadata: { contentType: 'image/jpeg', mediaType: 'image' },
|
|
},
|
|
deduplicated: false,
|
|
});
|
|
};
|
|
|
|
it('should create video job and poll for completion', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
setupMocksForSuccess();
|
|
|
|
const prompt = 'A cat riding a skateboard';
|
|
const result = await provider.callApi(prompt);
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.video).toBeDefined();
|
|
expect(result.video?.id).toBe('video_123');
|
|
expect(result.video?.format).toBe('mp4');
|
|
// Verify structured storageRef object (preferred)
|
|
expect(result.video?.storageRef).toBeDefined();
|
|
expect(result.video?.storageRef?.key).toBe('video/abc123.mp4');
|
|
// Verify legacy URL format for backwards compatibility
|
|
expect(result.video?.url).toContain('storageRef:');
|
|
expect(result.video?.thumbnail).toContain('storageRef:');
|
|
expect(result.video?.spritesheet).toContain('storageRef:');
|
|
expect(result.cost).toBeGreaterThan(0);
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringMatching(/\/videos$/),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'X-OpenAI-Originator': 'promptfoo',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle invalid video size', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', size: '1920x1080' as any },
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.error).toContain('Invalid video size');
|
|
});
|
|
|
|
it('should handle job creation failure', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 400,
|
|
statusText: 'Bad Request',
|
|
json: async () => ({ error: { message: 'Invalid prompt' } }),
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.error).toContain('Invalid prompt');
|
|
});
|
|
|
|
it('should handle job failed status', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Mock polling - failed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 'video_123',
|
|
status: 'failed',
|
|
error: { message: 'Content policy violation' },
|
|
}),
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.error).toContain('Content policy violation');
|
|
});
|
|
|
|
it('should handle polling timeout', async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: {
|
|
apiKey: 'test-key',
|
|
poll_interval_ms: 10,
|
|
max_poll_time_ms: 50,
|
|
},
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Always return in_progress to trigger timeout
|
|
mockFetchWithProxy.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'in_progress', progress: 10 }),
|
|
});
|
|
|
|
const resultPromise = provider.callApi('test prompt');
|
|
await vi.runAllTimersAsync();
|
|
const result = await resultPromise;
|
|
|
|
expect(result.error).toContain('timed out');
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it('should throw error if API key is not set', async () => {
|
|
const restoreEnv = mockProcessEnv({ OPENAI_API_KEY: undefined });
|
|
|
|
try {
|
|
const provider = new OpenAiVideoProvider('sora-2');
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow(
|
|
getOpenAiMissingApiKeyMessage(),
|
|
);
|
|
} finally {
|
|
restoreEnv();
|
|
}
|
|
});
|
|
|
|
it('should use custom apiKeyEnvar in missing API key errors', async () => {
|
|
const restoreEnv = mockProcessEnv({
|
|
OPENAI_API_KEY: undefined,
|
|
CUSTOM_VIDEO_API_KEY: undefined,
|
|
});
|
|
|
|
try {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: {
|
|
apiKeyEnvar: 'CUSTOM_VIDEO_API_KEY',
|
|
},
|
|
env: {
|
|
OPENAI_API_KEY: undefined,
|
|
CUSTOM_VIDEO_API_KEY: undefined,
|
|
},
|
|
});
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow(
|
|
getOpenAiMissingApiKeyMessage('CUSTOM_VIDEO_API_KEY'),
|
|
);
|
|
} finally {
|
|
restoreEnv();
|
|
}
|
|
});
|
|
|
|
it('should handle video download failure', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Mock polling - completed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'completed', progress: 100 }),
|
|
});
|
|
|
|
// Mock video download failure
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: 'Internal Server Error',
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.error).toContain('Failed to download video');
|
|
});
|
|
|
|
it('should use default model sora-2 when not specified', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
setupMocksForSuccess();
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
// Verify the first fetch call (job creation) includes sora-2 model
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos'),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"model":"sora-2"'),
|
|
}),
|
|
);
|
|
expect(result.video?.model).toBe('sora-2');
|
|
});
|
|
|
|
it('should use specified size and seconds', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', size: '1792x1024', seconds: 12 },
|
|
});
|
|
|
|
setupMocksForSuccess();
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos'),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"size":"1792x1024"'),
|
|
}),
|
|
);
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos'),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"seconds":"12"'),
|
|
}),
|
|
);
|
|
expect(result.video?.size).toBe('1792x1024');
|
|
expect(result.video?.duration).toBe(12);
|
|
});
|
|
|
|
it('should skip thumbnail download when disabled', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', download_thumbnail: false },
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Mock polling - completed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'completed', progress: 100 }),
|
|
});
|
|
|
|
// Mock video download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
|
|
// Mock spritesheet download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia
|
|
.mockResolvedValueOnce({
|
|
ref: { provider: 'local', key: 'video/abc123.mp4', contentHash: 'abc123', metadata: {} },
|
|
deduplicated: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ref: { provider: 'local', key: 'video/abc123.jpg', contentHash: 'abc123', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.video?.thumbnail).toBeUndefined();
|
|
expect(result.video?.spritesheet).toBeDefined();
|
|
});
|
|
|
|
it('should skip spritesheet download when disabled', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', download_spritesheet: false },
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Mock polling - completed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'completed', progress: 100 }),
|
|
});
|
|
|
|
// Mock video download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
|
|
// Mock thumbnail download
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia
|
|
.mockResolvedValueOnce({
|
|
ref: { provider: 'local', key: 'video/abc123.mp4', contentHash: 'abc123', metadata: {} },
|
|
deduplicated: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ref: { provider: 'local', key: 'video/abc123.webp', contentHash: 'abc123', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result.video?.thumbnail).toBeDefined();
|
|
expect(result.video?.spritesheet).toBeUndefined();
|
|
});
|
|
|
|
it('should continue even if thumbnail/spritesheet download fails', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
// Mock job creation
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'queued' }),
|
|
});
|
|
|
|
// Mock polling - completed
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_123', status: 'completed', progress: 100 }),
|
|
});
|
|
|
|
// Mock video download - success
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
|
|
// Mock thumbnail download - failure
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
statusText: 'Not Found',
|
|
});
|
|
|
|
// Mock spritesheet download - failure
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
statusText: 'Not Found',
|
|
});
|
|
|
|
// Mock storage for video only
|
|
mockStoreMedia.mockResolvedValueOnce({
|
|
ref: { provider: 'local', key: 'video/abc123.mp4', contentHash: 'abc123', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
// Should still succeed, just without thumbnail/spritesheet
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.video).toBeDefined();
|
|
expect(result.video?.thumbnail).toBeUndefined();
|
|
expect(result.video?.spritesheet).toBeUndefined();
|
|
});
|
|
|
|
it('should sanitize prompt in output', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
setupMocksForSuccess();
|
|
|
|
const result = await provider.callApi('Test [prompt] with\nnewlines');
|
|
|
|
expect(result.output).toContain('Test (prompt) with newlines');
|
|
});
|
|
});
|
|
|
|
describe('generateVideoCacheKey', () => {
|
|
it('should generate deterministic cache key for same inputs', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys for different prompts', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'prompt one',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'prompt two',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys for different models', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2-pro',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys for different sizes', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '720x1280',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys for different durations', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 4,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys with and without input_reference', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
inputReference: 'base64imagedata',
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should generate different keys for different providers', () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'azure',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
|
|
it('should return 12-character hex string', () => {
|
|
const key = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'test prompt',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
});
|
|
|
|
// Should be 12 hex characters
|
|
expect(key).toMatch(/^[0-9a-f]{12}$/);
|
|
});
|
|
});
|
|
|
|
describe('checkVideoCache', () => {
|
|
it('should return video key if cache mapping exists and video file exists', async () => {
|
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
|
JSON.stringify({
|
|
videoKey: 'video/abc123def456.mp4',
|
|
thumbnailKey: 'video/abc123def456.webp',
|
|
}),
|
|
);
|
|
|
|
// Mock storage: video file exists
|
|
const mockStorage = {
|
|
exists: vi.fn().mockImplementation((key: string) => {
|
|
if (key === 'video/abc123def456.mp4') {
|
|
return Promise.resolve(true);
|
|
}
|
|
return Promise.resolve(false);
|
|
}),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
const result = await checkVideoCache('test-cache-key');
|
|
|
|
expect(result).toBe('video/abc123def456.mp4');
|
|
expect(fsPromises.readFile).toHaveBeenCalled();
|
|
expect(mockStorage.exists).toHaveBeenCalledWith('video/abc123def456.mp4');
|
|
});
|
|
|
|
it('should return null if cache mapping does not exist', async () => {
|
|
const result = await checkVideoCache('nonexistent-key');
|
|
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
it('should return null if video file no longer exists', async () => {
|
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
|
JSON.stringify({
|
|
videoKey: 'video/deleted123.mp4',
|
|
}),
|
|
);
|
|
|
|
// Mock storage: video file was deleted
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
const result = await checkVideoCache('test-key');
|
|
|
|
expect(result).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('caching behavior', () => {
|
|
it('should return cached result when video already exists', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
|
JSON.stringify({
|
|
videoKey: 'video/stored123.mp4',
|
|
}),
|
|
);
|
|
|
|
// Mock storage: video file exists
|
|
const mockStorage = {
|
|
exists: vi.fn().mockImplementation((key: string) => {
|
|
if (key === 'video/stored123.mp4') {
|
|
return Promise.resolve(true);
|
|
}
|
|
return Promise.resolve(false);
|
|
}),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
const result = await provider.callApi('cached video prompt');
|
|
|
|
// Should not make any API calls
|
|
expect(mockFetchWithProxy).not.toHaveBeenCalled();
|
|
|
|
// Should return cached response
|
|
expect(result.cached).toBe(true);
|
|
expect(result.cost).toBe(0);
|
|
expect(result.latencyMs).toBe(0);
|
|
expect(result.video).toBeDefined();
|
|
expect(result.video?.url).toBe('storageRef:video/stored123.mp4');
|
|
expect(result.video?.format).toBe('mp4');
|
|
expect(result.video?.model).toBe('sora-2');
|
|
});
|
|
|
|
it('should include thumbnail path in cached result if thumbnail exists', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
|
JSON.stringify({
|
|
videoKey: 'video/stored123.mp4',
|
|
thumbnailKey: 'video/stored123.webp',
|
|
spritesheetKey: 'video/stored123.jpg',
|
|
}),
|
|
);
|
|
|
|
// Mock storage: all asset files exist
|
|
const mockStorage = {
|
|
exists: vi.fn().mockImplementation((key: string) => {
|
|
if (
|
|
key === 'video/stored123.mp4' ||
|
|
key === 'video/stored123.webp' ||
|
|
key === 'video/stored123.jpg'
|
|
) {
|
|
return Promise.resolve(true);
|
|
}
|
|
return Promise.resolve(false);
|
|
}),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
const result = await provider.callApi('cached video with assets');
|
|
|
|
expect(result.cached).toBe(true);
|
|
expect(result.video?.thumbnail).toBe('storageRef:video/stored123.webp');
|
|
expect(result.video?.spritesheet).toBe('storageRef:video/stored123.jpg');
|
|
});
|
|
|
|
it('should not include thumbnail in cached result if it does not exist', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key' },
|
|
});
|
|
|
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
|
JSON.stringify({
|
|
videoKey: 'video/stored123.mp4',
|
|
thumbnailKey: 'video/stored123.webp',
|
|
spritesheetKey: 'video/stored123.jpg',
|
|
}),
|
|
);
|
|
|
|
// Mock storage: only video exists, not thumbnail/spritesheet
|
|
const mockStorage = {
|
|
exists: vi.fn().mockImplementation((key: string) => {
|
|
if (key === 'video/stored123.mp4') {
|
|
return Promise.resolve(true);
|
|
}
|
|
return Promise.resolve(false);
|
|
}),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
const result = await provider.callApi('cached video without extras');
|
|
|
|
expect(result.cached).toBe(true);
|
|
expect(result.video?.thumbnail).toBeUndefined();
|
|
expect(result.video?.spritesheet).toBeUndefined();
|
|
});
|
|
|
|
it('should skip caching for remix operations', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', remix_video_id: 'existing_video_123' },
|
|
});
|
|
|
|
// Even though storage indicates file exists, remix should not use cache
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
store: vi.fn(),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
// Setup mocks for API calls (remix should make API calls)
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'queued' }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'completed', progress: 100 }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia.mockResolvedValue({
|
|
ref: { provider: 'local', key: 'video/abc.mp4', contentHash: 'abc', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
const result = await provider.callApi('remix this video with new style');
|
|
|
|
// Should make API calls (not use cache)
|
|
expect(mockFetchWithProxy).toHaveBeenCalled();
|
|
expect(result.cached).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('input_reference (image-to-video)', () => {
|
|
it('should include input_reference in request body for base64 data', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', input_reference: 'base64EncodedImageData' },
|
|
});
|
|
|
|
// Mock storage to indicate no cache
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
// Setup mocks for success
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_img2vid', status: 'queued' }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_img2vid', status: 'completed', progress: 100 }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia.mockResolvedValue({
|
|
ref: { provider: 'local', key: 'video/abc.mp4', contentHash: 'abc', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
await provider.callApi('Animate this image');
|
|
|
|
// Verify the request body includes input_reference
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos'),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"input_reference":"base64EncodedImageData"'),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should read and base64 encode file when input_reference starts with file://', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', input_reference: 'file:///path/to/image.png' },
|
|
});
|
|
|
|
// Mock storage
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
|
|
if (String(filePath) === '/path/to/image.png') {
|
|
return Buffer.from('fake image data');
|
|
}
|
|
throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
});
|
|
|
|
// Setup mocks for success
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_from_file', status: 'queued' }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'video_from_file', status: 'completed', progress: 100 }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia.mockResolvedValue({
|
|
ref: { provider: 'local', key: 'video/abc.mp4', contentHash: 'abc', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
await provider.callApi('Animate this file');
|
|
|
|
// Verify the file was read
|
|
expect(fsPromises.readFile).toHaveBeenCalledWith('/path/to/image.png');
|
|
|
|
// Verify the request body includes base64 encoded data
|
|
const expectedBase64 = Buffer.from('fake image data').toString('base64');
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos'),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining(`"input_reference":"${expectedBase64}"`),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should return error if input_reference file does not exist', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', input_reference: 'file:///nonexistent/image.png' },
|
|
});
|
|
|
|
// Mock storage
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
vi.mocked(fsPromises.readFile).mockRejectedValue(
|
|
Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' }),
|
|
);
|
|
|
|
const result = await provider.callApi('Animate nonexistent file');
|
|
|
|
expect(result.error).toContain('Input reference file not found');
|
|
expect(result.error).toContain('/nonexistent/image.png');
|
|
});
|
|
|
|
it('should generate different cache keys for same prompt with different input_reference', async () => {
|
|
const key1 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'animate',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
inputReference: 'imageDataA',
|
|
});
|
|
const key2 = generateVideoCacheKey({
|
|
provider: 'openai',
|
|
prompt: 'animate',
|
|
model: 'sora-2',
|
|
size: '1280x720',
|
|
seconds: 8,
|
|
inputReference: 'imageDataB',
|
|
});
|
|
|
|
expect(key1).not.toBe(key2);
|
|
});
|
|
});
|
|
|
|
describe('remix_video_id', () => {
|
|
it('should use remix endpoint when remix_video_id is provided', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: { apiKey: 'test-key', remix_video_id: 'original_video_456' },
|
|
});
|
|
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(true),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
// Setup mocks for remix
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'queued' }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'completed', progress: 100 }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia.mockResolvedValue({
|
|
ref: { provider: 'local', key: 'video/abc.mp4', contentHash: 'abc', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
await provider.callApi('Make it more colorful');
|
|
|
|
// Verify the remix endpoint was called
|
|
expect(mockFetchWithProxy).toHaveBeenCalledWith(
|
|
expect.stringContaining('/videos/original_video_456/remix'),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it('should not include size and seconds in remix request', async () => {
|
|
const provider = new OpenAiVideoProvider('sora-2', {
|
|
config: {
|
|
apiKey: 'test-key',
|
|
remix_video_id: 'original_video_789',
|
|
size: '720x1280',
|
|
seconds: 12,
|
|
},
|
|
});
|
|
|
|
const mockStorage = {
|
|
exists: vi.fn().mockResolvedValue(true),
|
|
};
|
|
mockGetMediaStorage.mockReturnValue(mockStorage);
|
|
|
|
// Setup mocks for remix
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'queued' }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'remixed_video', status: 'completed', progress: 100 }),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(100),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(50),
|
|
});
|
|
mockFetchWithProxy.mockResolvedValueOnce({
|
|
ok: true,
|
|
arrayBuffer: async () => new ArrayBuffer(75),
|
|
});
|
|
|
|
// Mock storage
|
|
mockStoreMedia.mockResolvedValue({
|
|
ref: { provider: 'local', key: 'video/abc.mp4', contentHash: 'abc', metadata: {} },
|
|
deduplicated: false,
|
|
});
|
|
|
|
await provider.callApi('Change the style');
|
|
|
|
// Get the first call (job creation for remix)
|
|
const [, options] = mockFetchWithProxy.mock.calls[0];
|
|
const body = JSON.parse(options.body);
|
|
|
|
// Remix requests should not include size and seconds
|
|
expect(body.size).toBeUndefined();
|
|
expect(body.seconds).toBeUndefined();
|
|
// But should include model and prompt
|
|
expect(body.model).toBe('sora-2');
|
|
expect(body.prompt).toBe('Change the style');
|
|
});
|
|
});
|
|
});
|