Files
promptfoo--promptfoo/test/providers/xai/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

879 lines
30 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as videoUtils from '../../../src/providers/video/utils';
import {
calculateVideoCost,
createXAIVideoProvider,
validateAspectRatio,
validateDuration,
validateResolution,
XAIVideoProvider,
} from '../../../src/providers/xai/video';
import * as fetch from '../../../src/util/fetch/index';
import { sleep } from '../../../src/util/time';
vi.mock('../../../src/logger');
vi.mock('../../../src/util/fetch/index');
// Only mock specific functions from video utils, not the validators
vi.mock('../../../src/providers/video/utils', async (importOriginal) => {
const actual = await importOriginal<typeof videoUtils>();
return {
...actual,
checkVideoCache: vi.fn(),
generateVideoCacheKey: vi.fn(),
buildStorageRefUrl: vi.fn(),
formatVideoOutput: vi.fn(),
storeCacheMapping: vi.fn(),
storeVideoContent: vi.fn(),
};
});
vi.mock('../../../src/util/time', () => ({
sleep: vi.fn().mockResolvedValue(undefined),
}));
describe('XAI Video Provider', () => {
const mockApiKey = 'test-xai-api-key';
const mockPrompt = 'A red ball bouncing';
const mockRequestId = 'test-request-id-123';
const mockVideoUrl = 'https://vidgen.x.ai/test-video.mp4';
const mockStorageKey = 'video/abc123.mp4';
beforeEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
vi.mocked(sleep).mockResolvedValue(undefined);
// Mock environment
vi.stubEnv('XAI_API_KEY', mockApiKey);
// Mock video utils
vi.mocked(videoUtils.checkVideoCache).mockResolvedValue(null);
vi.mocked(videoUtils.generateVideoCacheKey).mockReturnValue('test-cache-key');
vi.mocked(videoUtils.buildStorageRefUrl).mockReturnValue(`storageRef:${mockStorageKey}`);
vi.mocked(videoUtils.formatVideoOutput).mockReturnValue(
`[Video: ${mockPrompt}](storageRef:${mockStorageKey})`,
);
vi.mocked(videoUtils.storeCacheMapping).mockResolvedValue(undefined);
vi.mocked(videoUtils.storeVideoContent).mockResolvedValue({
storageRef: {
provider: 'filesystem',
key: mockStorageKey,
contentHash: 'test-hash',
metadata: { contentType: 'video/mp4', mediaType: 'video', sizeBytes: 100000 },
},
});
});
afterEach(() => {
vi.resetAllMocks();
vi.unstubAllEnvs();
});
describe('Validation', () => {
describe('validateAspectRatio', () => {
it('accepts valid aspect ratios', () => {
expect(validateAspectRatio('16:9')).toEqual({ valid: true });
expect(validateAspectRatio('4:3')).toEqual({ valid: true });
expect(validateAspectRatio('1:1')).toEqual({ valid: true });
expect(validateAspectRatio('9:16')).toEqual({ valid: true });
expect(validateAspectRatio('3:4')).toEqual({ valid: true });
expect(validateAspectRatio('3:2')).toEqual({ valid: true });
expect(validateAspectRatio('2:3')).toEqual({ valid: true });
});
it('rejects invalid aspect ratios', () => {
const result = validateAspectRatio('21:9' as any);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid aspect ratio');
});
});
describe('validateResolution', () => {
it('accepts valid resolutions', () => {
expect(validateResolution('720p')).toEqual({ valid: true });
expect(validateResolution('480p')).toEqual({ valid: true });
});
it('rejects invalid resolutions', () => {
const result = validateResolution('1080p' as any);
expect(result.valid).toBe(false);
expect(result.message).toContain('Invalid resolution');
});
});
describe('validateDuration', () => {
it('accepts valid durations (1-15)', () => {
expect(validateDuration(1)).toEqual({ valid: true });
expect(validateDuration(8)).toEqual({ valid: true });
expect(validateDuration(15)).toEqual({ valid: true });
});
it('rejects duration below minimum', () => {
const result = validateDuration(0);
expect(result.valid).toBe(false);
expect(result.message).toContain('between 1 and 15');
});
it('rejects duration above maximum', () => {
const result = validateDuration(16);
expect(result.valid).toBe(false);
expect(result.message).toContain('between 1 and 15');
});
});
describe('calculateVideoCost', () => {
it('calculates cost based on duration', () => {
expect(calculateVideoCost(3)).toBeCloseTo(0.15, 2); // 3 seconds * $0.05
expect(calculateVideoCost(10)).toBeCloseTo(0.5, 2); // 10 seconds * $0.05
});
it('returns 0 for cached videos', () => {
expect(calculateVideoCost(10, true)).toBe(0);
});
});
});
describe('Provider creation and configuration', () => {
it('creates provider with default model', () => {
const provider = createXAIVideoProvider('xai:video:');
expect(provider).toBeInstanceOf(XAIVideoProvider);
expect(provider.id()).toBe('xai:video:grok-imagine-video');
});
it('creates provider with specified model', () => {
const provider = createXAIVideoProvider('xai:video:grok-imagine-video');
expect(provider.id()).toBe('xai:video:grok-imagine-video');
});
it('creates provider with custom ID', () => {
const provider = new XAIVideoProvider('grok-imagine-video', { id: 'my-custom-id' });
expect(provider.id()).toBe('my-custom-id');
});
it('uses correct API URL', () => {
const provider = new XAIVideoProvider('grok-imagine-video');
expect(provider.getApiUrl()).toBe('https://api.x.ai/v1');
});
it('uses custom API URL from config', () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { apiBaseUrl: 'https://custom.api.example.com' },
});
expect(provider.getApiUrl()).toBe('https://custom.api.example.com');
});
it('uses a regional API URL when configured', () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { region: 'eu-west-1' },
});
expect(provider.getApiUrl()).toBe('https://eu-west-1.api.x.ai/v1');
});
it('returns correct string representation', () => {
const provider = new XAIVideoProvider('grok-imagine-video');
expect(provider.toString()).toBe('[xAI Video Provider grok-imagine-video]');
});
});
describe('API key handling', () => {
it('uses API key from config', () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { apiKey: 'custom-api-key' },
});
expect(provider.getApiKey()).toBe('custom-api-key');
});
it('uses API key from environment', () => {
const provider = new XAIVideoProvider('grok-imagine-video');
expect(provider.getApiKey()).toBe(mockApiKey);
});
it('returns error when API key is missing', async () => {
// Explicitly set API key to empty to simulate missing key
vi.stubEnv('XAI_API_KEY', '');
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('XAI_API_KEY');
});
});
describe('Video generation flow', () => {
it('generates video successfully', async () => {
// Mock job creation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Mock status poll - completed
const pollResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
status: 'done',
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
usage: { cost_in_usd_ticks: 123000000 },
}),
};
// Mock video download
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any) // Create job
.mockResolvedValueOnce(pollResponse as any) // Poll status
.mockResolvedValueOnce(downloadResponse as any); // Download video
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toBeUndefined();
expect(result.output).toContain('Video:');
expect(result.video?.model).toBe('grok-imagine-video');
expect(result.cost).toBe(0.0123);
expect(result.cached).toBe(false);
});
it('handles pending status and polls until completed', async () => {
// Mock job creation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Mock pending status
const pendingResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ status: 'pending' }),
};
// Mock completed status
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 5 },
model: 'grok-imagine-video',
}),
};
// Mock video download
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(pendingResponse as any) // First poll - pending
.mockResolvedValueOnce(completedResponse as any) // Second poll - completed
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toBeUndefined();
expect(result.video?.duration).toBe(5);
});
it('returns cached result when available', async () => {
vi.mocked(videoUtils.checkVideoCache).mockResolvedValue(mockStorageKey);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.cached).toBe(true);
expect(result.cost).toBe(0);
expect(result.latencyMs).toBe(0);
expect(fetch.fetchWithProxy).not.toHaveBeenCalled();
});
});
describe('Error handling', () => {
it('handles job creation failure', async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: 'Bad Request',
json: vi.fn().mockResolvedValue({ error: { message: 'Invalid prompt' } }),
};
vi.mocked(fetch.fetchWithProxy).mockResolvedValue(errorResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('API error 400');
expect(result.error).toContain('Invalid prompt');
});
it('handles polling timeout', async () => {
vi.useFakeTimers();
try {
// Route the mocked sleep through the real implementation so fake
// timers can advance the polling loop deterministically.
const realTime =
await vi.importActual<typeof import('../../../src/util/time')>('../../../src/util/time');
vi.mocked(sleep).mockImplementation(realTime.sleep);
// Mock job creation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Mock pending status that never completes
const pendingResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ status: 'pending' }),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValue(pendingResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { max_poll_time_ms: 100, poll_interval_ms: 10 },
});
const resultPromise = provider.callApi(mockPrompt);
await vi.runAllTimersAsync();
const result = await resultPromise;
expect(result.error).toContain('timed out');
} finally {
vi.useRealTimers();
}
});
it('handles failed video generation', async () => {
// Mock job creation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Mock failed status
const failedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ status: 'failed', error: 'Content policy violation' }),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(failedResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Content policy violation');
});
it('handles video download failure', async () => {
// Mock job creation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Mock completed status
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
// Mock download failure
const downloadResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Failed to download video');
});
it('handles invalid duration parameter', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { duration: 20 }, // Invalid - max is 15
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Invalid duration');
});
it('handles invalid aspect ratio parameter', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { aspect_ratio: '21:9' as any },
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Invalid aspect ratio');
});
it('handles invalid resolution parameter', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { resolution: '4K' as any },
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Invalid resolution');
});
});
describe('Image-to-video generation', () => {
it('includes image URL in request', async () => {
const imageUrl = 'https://example.com/image.jpg';
// Mock responses
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { image: { url: imageUrl } },
});
await provider.callApi(mockPrompt);
// Verify the request included the image
const calls = vi.mocked(fetch.fetchWithProxy).mock.calls;
const createCall = calls[0];
const body = JSON.parse(createCall[1]?.body as string);
expect(body.image).toEqual({ url: imageUrl });
});
});
describe('Reference-to-video generation', () => {
it('includes reference images in the request', async () => {
const referenceImages = [
{ url: 'https://example.com/person.jpg' },
{ url: 'https://example.com/shirt.jpg' },
];
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { reference_images: referenceImages, duration: 10 },
});
const result = await provider.callApi(mockPrompt);
const createCall = vi.mocked(fetch.fetchWithProxy).mock.calls[0];
const body = JSON.parse(createCall[1]?.body as string);
expect(body.reference_images).toEqual(referenceImages);
expect(result.metadata?.hasReferenceImages).toBe(true);
});
it('rejects reference images combined with image-to-video', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: {
image: { url: 'https://example.com/frame.jpg' },
reference_images: [{ url: 'https://example.com/reference.jpg' }],
},
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('reference_images cannot be combined with image input');
expect(fetch.fetchWithProxy).not.toHaveBeenCalled();
});
it('rejects more than seven reference images', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: {
reference_images: Array.from({ length: 8 }, (_, index) => ({
url: `https://example.com/reference-${index}.jpg`,
})),
},
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Must be between 1 and 7');
expect(fetch.fetchWithProxy).not.toHaveBeenCalled();
});
it('rejects reference image requests longer than ten seconds', async () => {
const provider = new XAIVideoProvider('grok-imagine-video', {
config: {
reference_images: [{ url: 'https://example.com/reference.jpg' }],
duration: 11,
},
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('for reference_images');
expect(fetch.fetchWithProxy).not.toHaveBeenCalled();
});
});
describe('Video editing', () => {
it('uses edit endpoint when video URL is provided', async () => {
const sourceVideoUrl = 'https://example.com/source.mp4';
// Mock responses
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { video: { url: sourceVideoUrl } },
});
await provider.callApi('Make the ball larger');
// Verify the edit endpoint was used
const calls = vi.mocked(fetch.fetchWithProxy).mock.calls;
const createCall = calls[0];
expect(createCall[0]).toContain('/videos/edits');
// Verify video URL was included
const body = JSON.parse(createCall[1]?.body as string);
expect(body.video).toEqual({ url: sourceVideoUrl });
});
it('skips cache for video edits', async () => {
const sourceVideoUrl = 'https://example.com/source.mp4';
// Mock responses
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { video: { url: sourceVideoUrl } },
});
const result = await provider.callApi('Make the ball larger');
// storeCacheMapping should not be called for edits
expect(videoUtils.storeCacheMapping).not.toHaveBeenCalled();
expect(result.metadata?.isEdit).toBe(true);
});
it('does not validate duration/aspect_ratio for edits', async () => {
const sourceVideoUrl = 'https://example.com/source.mp4';
// Mock responses
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
// Even with invalid duration, edits should work
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { video: { url: sourceVideoUrl }, duration: 999 },
});
const result = await provider.callApi('Edit prompt');
// Should not return validation error for edits
expect(result.error).toBeUndefined();
});
});
describe('Cache key generation', () => {
it('generates cache key with correct parameters', async () => {
// Mock responses for successful generation
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 5 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { duration: 5, aspect_ratio: '9:16', resolution: '480p' },
});
await provider.callApi(mockPrompt);
expect(videoUtils.generateVideoCacheKey).toHaveBeenCalledWith({
provider: 'xai',
prompt: mockPrompt,
model: 'grok-imagine-video',
size: '9:16:480p',
seconds: 5,
inputReference: null,
});
});
it('includes image URL in cache key for image-to-video', async () => {
const imageUrl = 'https://example.com/image.jpg';
vi.mocked(videoUtils.checkVideoCache).mockResolvedValue(mockStorageKey);
const provider = new XAIVideoProvider('grok-imagine-video', {
config: { image: { url: imageUrl } },
});
await provider.callApi(mockPrompt);
expect(videoUtils.generateVideoCacheKey).toHaveBeenCalledWith(
expect.objectContaining({
inputReference: `image:${imageUrl}`,
}),
);
});
it('uses distinct cache references for image and reference-image modes', async () => {
const sharedUrl = 'https://example.com/shared.jpg';
vi.mocked(videoUtils.checkVideoCache).mockResolvedValue('cached-video-key');
await new XAIVideoProvider('grok-imagine-video', {
config: { image: { url: sharedUrl } },
}).callApi(mockPrompt);
await new XAIVideoProvider('grok-imagine-video', {
config: { reference_images: [{ url: sharedUrl }] },
}).callApi(mockPrompt);
expect(videoUtils.generateVideoCacheKey).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
inputReference: `image:${sharedUrl}`,
}),
);
expect(videoUtils.generateVideoCacheKey).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
inputReference: `reference_images:${sharedUrl}`,
}),
);
});
});
describe('Additional error scenarios', () => {
it('handles network exception during job creation', async () => {
vi.mocked(fetch.fetchWithProxy).mockRejectedValueOnce(new Error('Network error'));
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Failed to create video job');
expect(result.error).toContain('Network error');
});
it('handles network exception during polling', async () => {
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockRejectedValueOnce(new Error('Connection reset'));
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Polling error');
expect(result.error).toContain('Connection reset');
});
it('handles HTTP error during status polling', async () => {
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const errorResponse = {
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: vi.fn().mockResolvedValue({ error: { message: 'Server overloaded' } }),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(errorResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Status check failed');
expect(result.error).toContain('Server overloaded');
});
it('handles storeVideoContent error', async () => {
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
// Mock storage failure
vi.mocked(videoUtils.storeVideoContent).mockResolvedValue({
error: 'Storage quota exceeded',
});
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toContain('Storage quota exceeded');
});
it('handles processing status before completion', async () => {
const createResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ request_id: mockRequestId }),
};
// Status goes: pending -> processing -> completed
const pendingResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ status: 'pending' }),
};
const processingResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ status: 'processing' }),
};
const completedResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
video: { url: mockVideoUrl, duration: 3 },
model: 'grok-imagine-video',
}),
};
const downloadResponse = {
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(1000)),
};
vi.mocked(fetch.fetchWithProxy)
.mockResolvedValueOnce(createResponse as any)
.mockResolvedValueOnce(pendingResponse as any)
.mockResolvedValueOnce(processingResponse as any)
.mockResolvedValueOnce(completedResponse as any)
.mockResolvedValueOnce(downloadResponse as any);
const provider = new XAIVideoProvider('grok-imagine-video');
const result = await provider.callApi(mockPrompt);
expect(result.error).toBeUndefined();
expect(result.video?.duration).toBe(3);
});
});
});