UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

614 lines (542 loc) • 17.2 kB
// @vitest-environment edge-runtime import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CreateImagePayload } from '../types/image'; import { CreateImageOptions } from '../utils/openaiCompatibleFactory'; import { createQwenImage } from './createImage'; // Mock the console.error to avoid polluting test output vi.spyOn(console, 'error').mockImplementation(() => {}); const mockOptions: CreateImageOptions = { apiKey: 'test-api-key', provider: 'qwen', }; beforeEach(() => { // Reset all mocks before each test vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('createQwenImage', () => { describe('Success scenarios', () => { it('should successfully generate image with immediate success', async () => { const mockTaskId = 'task-123456'; const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/test-image.jpg'; // Mock fetch for task creation and immediate success global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-123', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: mockImageUrl }], }, request_id: 'req-124', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'A beautiful sunset over the mountains', }, }; const result = await createQwenImage(payload, mockOptions); // Verify task creation request expect(fetch).toHaveBeenCalledWith( 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis', { method: 'POST', headers: { 'Authorization': 'Bearer test-api-key', 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable', }, body: JSON.stringify({ input: { prompt: 'A beautiful sunset over the mountains', }, model: 'wanx2.1-t2i-turbo', parameters: { n: 1, size: '1024*1024', }, }), }, ); // Verify status query request expect(fetch).toHaveBeenCalledWith( `https://dashscope.aliyuncs.com/api/v1/tasks/${mockTaskId}`, { headers: { Authorization: 'Bearer test-api-key', }, }, ); expect(result).toEqual({ imageUrl: mockImageUrl, }); }); it('should handle task that needs polling before success', async () => { const mockTaskId = 'task-polling'; const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/test-image-3.jpg'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-127', }), }) // First status query - still running .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'RUNNING', }, request_id: 'req-128', }), }) // Second status query - succeeded .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: mockImageUrl }], }, request_id: 'req-129', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Abstract digital art', }, }; const result = await createQwenImage(payload, mockOptions); // Should have made 3 fetch calls: 1 create + 2 status checks expect(fetch).toHaveBeenCalledTimes(3); expect(result).toEqual({ imageUrl: mockImageUrl, }); }); it('should handle custom image dimensions', async () => { const mockTaskId = 'task-custom-size'; const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/custom-size.jpg'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-140', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: mockImageUrl }], }, request_id: 'req-141', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Custom size image', width: 512, height: 768, }, }; await createQwenImage(payload, mockOptions); expect(fetch).toHaveBeenCalledWith( 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis', expect.objectContaining({ body: JSON.stringify({ input: { prompt: 'Custom size image', }, model: 'wanx2.1-t2i-turbo', parameters: { n: 1, size: '512*768', }, }), }), ); }); it('should handle long running tasks with retries', async () => { const mockTaskId = 'task-long-running'; // Mock status query that returns RUNNING a few times then succeeds let statusCallCount = 0; const statusQueryMock = vi.fn().mockImplementation(() => { statusCallCount++; if (statusCallCount <= 3) { return Promise.resolve({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'RUNNING', }, request_id: `req-${133 + statusCallCount}`, }), }); } else { return Promise.resolve({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: 'https://example.com/final-image.jpg' }], }, request_id: 'req-137', }), }); } }); global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-132', }), }) .mockImplementation(statusQueryMock); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Long running task', }, }; // Mock setTimeout to make test run faster but still allow controlled execution vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => { // Use setImmediate to avoid recursion issues setImmediate(callback); return 1 as any; }); const result = await createQwenImage(payload, mockOptions); expect(result).toEqual({ imageUrl: 'https://example.com/final-image.jpg', }); // Should have made 1 create call + 4 status calls (3 RUNNING + 1 SUCCEEDED) expect(fetch).toHaveBeenCalledTimes(5); }); it('should handle seed value of 0 correctly', async () => { const mockTaskId = 'task-with-zero-seed'; const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/seed-zero.jpg'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-seed-0', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: mockImageUrl }], }, request_id: 'req-seed-0-status', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Image with seed 0', seed: 0, }, }; await createQwenImage(payload, mockOptions); // Verify that seed: 0 is included in the request expect(fetch).toHaveBeenCalledWith( 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis', expect.objectContaining({ body: JSON.stringify({ input: { prompt: 'Image with seed 0', }, model: 'wanx2.1-t2i-turbo', parameters: { n: 1, seed: 0, size: '1024*1024', }, }), }), ); }); }); describe('Error scenarios', () => { it('should handle unsupported model', async () => { const payload: CreateImagePayload = { model: 'unsupported-model', params: { prompt: 'Test prompt', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); // Should not make any fetch calls expect(fetch).not.toHaveBeenCalled(); }); it('should handle task creation failure', async () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, statusText: 'Bad Request', json: async () => ({ message: 'Invalid model name', }), }); const payload: CreateImagePayload = { model: 'invalid-model', params: { prompt: 'Test prompt', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); }); it('should handle non-JSON error responses', async () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', json: async () => { throw new Error('Failed to parse JSON'); }, }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Test prompt', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); }); it('should handle task failure status', async () => { const mockTaskId = 'task-failed'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-130', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'FAILED', error_message: 'Content policy violation', }, request_id: 'req-131', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Invalid prompt that causes failure', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); }); it('should handle task succeeded but no results', async () => { const mockTaskId = 'task-no-results'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-134', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [], // Empty results array }, request_id: 'req-135', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Test prompt', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); }); it('should handle status query failure', async () => { const mockTaskId = 'task-query-fail'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-136', }), }) .mockResolvedValueOnce({ ok: false, statusText: 'Unauthorized', json: async () => ({ message: 'Invalid API key', }), }); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Test prompt', }, }; await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); }); it('should handle transient status query failures and retry', async () => { const mockTaskId = 'task-transient-failure'; const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/retry-success.jpg'; let statusQueryCount = 0; const statusQueryMock = vi.fn().mockImplementation(() => { statusQueryCount++; if (statusQueryCount === 1 || statusQueryCount === 2) { // First two calls fail return Promise.reject(new Error('Network timeout')); } else { // Third call succeeds return Promise.resolve({ ok: true, json: async () => ({ output: { task_id: mockTaskId, task_status: 'SUCCEEDED', results: [{ url: mockImageUrl }], }, request_id: 'req-retry-success', }), }); } }); global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-transient', }), }) .mockImplementation(statusQueryMock); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Test transient failure', }, }; // Mock setTimeout to make test run faster vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => { setImmediate(callback); return 1 as any; }); const result = await createQwenImage(payload, mockOptions); expect(result).toEqual({ imageUrl: mockImageUrl, }); // Verify the mock was called the expected number of times expect(statusQueryMock).toHaveBeenCalledTimes(3); // 2 failures + 1 success // Should have made 1 create call + 3 status calls (2 failed + 1 succeeded) expect(fetch).toHaveBeenCalledTimes(4); }); it('should fail after consecutive query failures', async () => { const mockTaskId = 'task-consecutive-failures'; global.fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ output: { task_id: mockTaskId }, request_id: 'req-will-fail', }), }) // All subsequent calls fail .mockRejectedValue(new Error('Persistent network error')); const payload: CreateImagePayload = { model: 'wanx2.1-t2i-turbo', params: { prompt: 'Test persistent failure', }, }; // Mock setTimeout to make test run faster vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => { setImmediate(callback); return 1 as any; }); await expect(createQwenImage(payload, mockOptions)).rejects.toEqual( expect.objectContaining({ errorType: 'ProviderBizError', provider: 'qwen', }), ); // Should have made 1 create call + 3 failed status calls (maxConsecutiveFailures) expect(fetch).toHaveBeenCalledTimes(4); }); }); });