import { afterEach, describe, expect, it, vi } from 'vitest'; import { createEditorAsset, createEditorAssetFolder, createEditorProject, createEditorProjectResource, deleteEditorAsset, deleteEditorAssetFolder, deleteEditorProject, editEditorImage, extractEditorUiDesignAssets, generateEditorBackgroundMusic, generateEditorCharacterAnimation, generateEditorIconSpritesheet, generateEditorImage, generateEditorSoundEffect, generateEditorVideo, listEditorProjects, loadEditorAssetLibrary, loadEditorProject, loadOrCreateRecentEditorProject, renameEditorProject, saveEditorProjectLayout, updateEditorAsset, updateEditorAssetFolder, } from './editorProjectClient'; const requestJsonMock = vi.hoisted(() => vi.fn()); vi.mock('../apiClient', () => ({ requestJson: requestJsonMock, })); describe('editorProjectClient', () => { afterEach(() => { requestJsonMock.mockReset(); }); it('loads the recent project without creating a duplicate when it exists', async () => { requestJsonMock.mockResolvedValueOnce({ project: { projectId: 'editor-project-1', title: '未命名画布', canvas: { canvasId: 'editor-project-1:canvas:default', projectId: 'editor-project-1', title: '默认画布', viewport: { x: 0, y: 0, scale: 1 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }); const project = await loadOrCreateRecentEditorProject(); expect(project.projectId).toBe('editor-project-1'); expect(requestJsonMock).toHaveBeenCalledTimes(1); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects/recent', { method: 'GET' }, '读取图片画布工程失败', ); }); it('creates a default project when there is no recent project', async () => { requestJsonMock .mockResolvedValueOnce({ project: null }) .mockResolvedValueOnce({ project: { projectId: 'editor-project-created', title: '未命名画布', canvas: { canvasId: 'editor-project-created:canvas:default', projectId: 'editor-project-created', title: '默认画布', viewport: { x: 0, y: 0, scale: 1 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }); const project = await loadOrCreateRecentEditorProject(); expect(project.projectId).toBe('editor-project-created'); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/editor/projects', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: '未命名画布' }), }), '创建图片画布工程失败', ); }); it('saves viewport and layer layout through the project API', async () => { requestJsonMock.mockResolvedValueOnce({ project: { projectId: 'editor-project-1', title: '未命名画布', canvas: { canvasId: 'editor-project-1:canvas:default', projectId: 'editor-project-1', title: '默认画布', viewport: { x: 12, y: 24, scale: 0.5 }, layers: [ { layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }, ], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 12, y: 24, scale: 0.5 }, layers: [ { layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }, ], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }); await saveEditorProjectLayout('editor-project-1', { viewport: { x: 12, y: 24, scale: 0.5 }, layers: [{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }], }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects/editor-project-1', expect.objectContaining({ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ viewport: { x: 12, y: 24, scale: 0.5 }, layers: [ { layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }, ], }), }), '保存图片画布工程失败', ); }); it('lists editor projects from the project API', async () => { requestJsonMock.mockResolvedValueOnce({ projects: [ { projectId: 'editor-project-1', title: '角色设定板', canvas: { canvasId: 'editor-project-1:canvas:default', projectId: 'editor-project-1', title: '默认画布', viewport: { x: 0, y: 0, scale: 1 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, ], }); const projects = await listEditorProjects(); expect(projects).toHaveLength(1); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects', { method: 'GET' }, '读取图片画布工程列表失败', ); }); it('loads an explicit project by id', async () => { requestJsonMock.mockResolvedValueOnce({ project: { projectId: 'editor-project-1', title: '角色设定板', canvas: { canvasId: 'editor-project-1:canvas:default', projectId: 'editor-project-1', title: '默认画布', viewport: { x: 8, y: 9, scale: 1.5 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 8, y: 9, scale: 1.5 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }); const project = await loadEditorProject('editor-project-1'); expect(project.viewport.scale).toBe(1.5); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects/editor-project-1', { method: 'GET' }, '读取图片画布工程失败', ); }); it('renames and deletes an editor project', async () => { requestJsonMock .mockResolvedValueOnce({ project: { projectId: 'editor-project-1', title: '新标题', canvas: { canvasId: 'editor-project-1:canvas:default', projectId: 'editor-project-1', title: '默认画布', viewport: { x: 0, y: 0, scale: 1 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }) .mockResolvedValueOnce({ deletedProjectId: 'editor-project-1' }); await renameEditorProject('editor-project-1', '新标题'); await deleteEditorProject('editor-project-1'); expect(requestJsonMock).toHaveBeenNthCalledWith( 1, '/api/editor/projects/editor-project-1/metadata', expect.objectContaining({ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: '新标题' }), }), '重命名图片画布工程失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/editor/projects/editor-project-1', { method: 'DELETE' }, '删除图片画布工程失败', ); }); it('creates a resource with upload or generated metadata', async () => { requestJsonMock.mockResolvedValueOnce({ resource: { resourceId: 'resource-generated-1', projectId: 'editor-project-1', imageSrc: '/generated-editor-assets/project/image.png', objectKey: 'generated-editor-assets/project/image.png', width: 2048, height: 2048, sourceType: 'generated', prompt: 'dragon knight', model: 'gpt-image-2', }, }); await createEditorProjectResource('editor-project-1', { imageSrc: '/generated-editor-assets/project/image.png', objectKey: 'generated-editor-assets/project/image.png', width: 2048, height: 2048, sourceType: 'generated', prompt: 'dragon knight', model: 'gpt-image-2', sourceResourceId: 'resource-source', }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects/editor-project-1/resources', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageSrc: '/generated-editor-assets/project/image.png', objectKey: 'generated-editor-assets/project/image.png', width: 2048, height: 2048, sourceType: 'generated', prompt: 'dragon knight', model: 'gpt-image-2', sourceResourceId: 'resource-source', }), }), '创建图片画布资源失败', ); }); it('loads and mutates the account-level asset library', async () => { requestJsonMock .mockResolvedValueOnce({ library: { folders: [ { folderId: 'folder-project', label: '项目素材', sortOrder: 0, collapsed: false, systemDefault: true, }, ], assets: [], }, }) .mockResolvedValueOnce({ folder: { folderId: 'folder-role', label: '角色', sortOrder: 100, collapsed: false, systemDefault: false, }, }) .mockResolvedValueOnce({ folder: { folderId: 'folder-role', label: '角色参考', sortOrder: 100, collapsed: true, systemDefault: false, }, }) .mockResolvedValueOnce({ library: { folders: [], assets: [], }, }); await loadEditorAssetLibrary(); await createEditorAssetFolder('角色', 100); await updateEditorAssetFolder('folder-role', { label: '角色参考', collapsed: true, }); await deleteEditorAssetFolder('folder-role'); expect(requestJsonMock).toHaveBeenNthCalledWith( 1, '/api/editor/assets/library', { method: 'GET' }, '读取图片画布素材库失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/editor/assets/folders', expect.objectContaining({ method: 'POST', body: JSON.stringify({ label: '角色', sortOrder: 100 }), }), '创建图片画布素材文件夹失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 3, '/api/editor/assets/folders/folder-role', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ label: '角色参考', collapsed: true }), }), '更新图片画布素材文件夹失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 4, '/api/editor/assets/folders/folder-role', { method: 'DELETE' }, '删除图片画布素材文件夹失败', ); }); it('creates, updates, and deletes account-level image assets', async () => { requestJsonMock .mockResolvedValueOnce({ asset: { assetId: 'asset-1', folderId: 'folder-project', label: '主视觉.png', imageSrc: 'data:image/png;base64,ZmFrZQ==', width: 640, height: 480, sourceType: 'uploaded', }, }) .mockResolvedValueOnce({ asset: { assetId: 'asset-1', folderId: 'folder-role', label: '角色主视觉.png', imageSrc: 'data:image/png;base64,ZmFrZQ==', width: 640, height: 480, sourceType: 'uploaded', }, }) .mockResolvedValueOnce({ asset: { assetId: 'asset-1', folderId: 'folder-role', label: '角色主视觉.png', imageSrc: 'data:image/png;base64,ZmFrZQ==', width: 640, height: 480, sourceType: 'uploaded', }, }); await createEditorAsset({ folderId: 'folder-project', label: '主视觉.png', imageSrc: 'data:image/png;base64,ZmFrZQ==', width: 640, height: 480, sourceType: 'uploaded', }); await updateEditorAsset('asset-1', { label: '角色主视觉.png', folderId: 'folder-role', }); await deleteEditorAsset('asset-1'); expect(requestJsonMock).toHaveBeenNthCalledWith( 1, '/api/editor/assets', expect.objectContaining({ method: 'POST', body: JSON.stringify({ folderId: 'folder-project', label: '主视觉.png', imageSrc: 'data:image/png;base64,ZmFrZQ==', width: 640, height: 480, sourceType: 'uploaded', }), }), '创建图片画布素材失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/editor/assets/asset-1', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ label: '角色主视觉.png', folderId: 'folder-role', }), }), '更新图片画布素材失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 3, '/api/editor/assets/asset-1', { method: 'DELETE' }, '删除图片画布素材失败', ); }); it('creates an explicit project from title input', async () => { requestJsonMock.mockResolvedValueOnce({ project: { projectId: 'editor-project-explicit', title: '角色设定板', canvas: { canvasId: 'editor-project-explicit:canvas:default', projectId: 'editor-project-explicit', title: '默认画布', viewport: { x: 0, y: 0, scale: 1 }, layers: [], updatedAt: '2026-06-12T00:00:00.000Z', }, viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T00:00:00.000Z', }, }); await createEditorProject({ title: '角色设定板' }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/projects', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: '角色设定板' }), }), '创建图片画布工程失败', ); }); it('generates editor images through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ imageSrc: 'data:image/png;base64,abc', width: 1024, height: 1024, sourceType: 'generated', prompt: '一张画布图片', actualPrompt: '一张画布图片', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'vector-task-1', }); const result = await generateEditorImage({ prompt: '一张画布图片' }); expect(result.taskId).toBe('vector-task-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '一张画布图片' }), }), '生成图片失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('passes image model options to editor image generation', async () => { requestJsonMock.mockResolvedValueOnce({ imageSrc: 'data:image/png;base64,character', width: 1024, height: 1536, sourceType: 'generated', prompt: '角色设定', actualPrompt: '角色设定', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'vector-character-1', }); await generateEditorImage({ prompt: '角色设定', kind: 'character', model: 'gpt-image-2', aspectRatio: '2:3', imageSize: '1K', }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '角色设定', kind: 'character', model: 'gpt-image-2', aspectRatio: '2:3', imageSize: '1K', }), }), '生成图片失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('generates icon spritesheets through the dedicated backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ spritesheetImageSrc: 'data:image/png;base64,sheet', spritesheetWidth: 512, spritesheetHeight: 512, iconImageSrcs: [ { name: '返回按钮', imageSrc: 'data:image/png;base64,back', width: 96, height: 96, }, ], prompt: '图标素材 prompt', actualPrompt: '图标素材 prompt', model: 'gemini-3.1-flash-image-preview', provider: 'VectorEngine', taskId: 'icon-spritesheet-task-1', priceMudPoints: 12, }); const result = await generateEditorIconSpritesheet({ referenceImageSrc: 'data:image/png;base64,spec', iconDescriptions: ['返回按钮', '设置按钮'], priceMudPoints: 12, }); expect(result.taskId).toBe('icon-spritesheet-task-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/icon-spritesheets/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ referenceImageSrc: 'data:image/png;base64,spec', iconDescriptions: ['返回按钮', '设置按钮'], model: 'gemini-3.1-flash-image-preview', priceMudPoints: 12, }), }), '生成图标素材失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('passes image model options to icon spritesheet generation', async () => { requestJsonMock.mockResolvedValueOnce({ spritesheetImageSrc: 'data:image/png;base64,sheet', spritesheetWidth: 1024, spritesheetHeight: 1024, iconImageSrcs: [], prompt: '图标素材 prompt', actualPrompt: '图标素材 prompt', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'icon-spritesheet-task-2', priceMudPoints: 20, }); await generateEditorIconSpritesheet({ referenceImageSrc: 'data:image/png;base64,spec', iconDescriptions: ['返回按钮'], model: 'gpt-image-2', aspectRatio: '1:1', imageSize: '2K', priceMudPoints: 20, }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/icon-spritesheets/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ referenceImageSrc: 'data:image/png;base64,spec', iconDescriptions: ['返回按钮'], model: 'gpt-image-2', aspectRatio: '1:1', imageSize: '2K', priceMudPoints: 20, }), }), '生成图标素材失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('extracts UI design assets through the dedicated backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ spritesheetImageSrc: 'data:image/png;base64,ui-sheet', spritesheetWidth: 1024, spritesheetHeight: 1024, iconImageSrcs: [ { name: '素材 1', imageSrc: 'data:image/png;base64,asset-1', width: 128, height: 128, }, ], prompt: '提取画面中的所有独立并整理成spritesheet', actualPrompt: '提取画面中的所有独立并整理成spritesheet', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'ui-asset-extraction-1', priceMudPoints: 20, }); const result = await extractEditorUiDesignAssets({ sourceImageSrc: 'data:image/png;base64,ui-design', }); expect(result.taskId).toBe('ui-asset-extraction-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/ui-designs/assets/extractions', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceImageSrc: 'data:image/png;base64,ui-design', }), }), '提取UI设计图素材失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('passes spec generation size and kind to the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ imageSrc: 'data:image/png;base64,spec', width: 2048, height: 1152, sourceType: 'generated', prompt: '生成规范图', actualPrompt: '生成规范图', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'vector-spec-1', }); const result = await generateEditorImage({ prompt: '生成规范图', size: '2048x1152', kind: 'spec', model: 'gpt-image-2', }); expect(result.width).toBe(2048); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '生成规范图', size: '2048x1152', kind: 'spec', model: 'gpt-image-2', }), }), '生成图片失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('passes publication material generation kind and size to the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ imageSrc: 'data:image/png;base64,publication', width: 720, height: 1280, sourceType: 'generated', prompt: '宣发素材 prompt', actualPrompt: '宣发素材 prompt', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'vector-publication-1', }); const result = await generateEditorImage({ prompt: '宣发素材 prompt', size: '720x1280', kind: 'publication-material', referenceImageSrcs: ['data:image/png;base64,ref'], }); expect(result.height).toBe(1280); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '宣发素材 prompt', size: '720x1280', kind: 'publication-material', referenceImageSrcs: ['data:image/png;base64,ref'], }), }), '生成图片失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('generates editor character animations through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ taskId: 'character-animation-1', model: 'seedance2.0-fast', prompt: '生成游戏角色动画\n动作描述:\n待机', previewVideoPath: '/generated-character-drafts/editor/preview.mp4', frames: [ { frameIndex: 1, imageSrc: '/generated-character-drafts/editor/frame01.png', width: 1024, height: 1024, }, ], frameCount: 32, durationSeconds: 4, fps: 8, priceMudPoints: 40, }); const result = await generateEditorCharacterAnimation({ sourceLayerId: 'layer-character', sourceImageSrc: 'data:image/png;base64,character', sourceWidth: 1024, sourceHeight: 1024, promptText: '待机', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, priceMudPoints: 40, model: 'seedance2.0-fast', }); expect(result.taskId).toBe('character-animation-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/character-animations/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceLayerId: 'layer-character', sourceImageSrc: 'data:image/png;base64,character', sourceWidth: 1024, sourceHeight: 1024, promptText: '待机', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, priceMudPoints: 40, model: 'seedance2.0-fast', }), }), '生成角色动画失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('generates editor videos through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ videoSrc: '/generated-editor-videos/video-task-1/preview.mp4', width: 1280, height: 720, sourceType: 'generated', prompt: '让角色向镜头挥手', actualPrompt: '让角色向镜头挥手', model: 'kling3.0-omni', provider: 'VectorEngine', taskId: 'video-task-1', durationSeconds: 5, resolution: '480p', priceMudPoints: 50, }); const result = await generateEditorVideo({ prompt: '让角色向镜头挥手', model: 'kling3.0-omni', aspectRatio: '16:9', durationSeconds: 5, resolution: '480p', mode: 'std', sound: 'off', priceMudPoints: 50, }); expect(result.taskId).toBe('video-task-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/videos/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '让角色向镜头挥手', model: 'kling3.0-omni', aspectRatio: '16:9', durationSeconds: 5, resolution: '480p', mode: 'std', sound: 'off', priceMudPoints: 50, }), }), '生成视频失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('passes seedance video multimodal references through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ videoSrc: '/generated-editor-videos/video-task-2/preview.mp4', width: 1280, height: 720, sourceType: 'generated', prompt: '参考素材生成视频', actualPrompt: '参考素材生成视频', model: 'seedance2.0-fast', provider: 'VectorEngine', taskId: 'video-task-2', durationSeconds: 5, resolution: '720p', priceMudPoints: 100, }); await generateEditorVideo({ prompt: '参考素材生成视频', model: 'seedance2.0-fast', aspectRatio: '16:9', durationSeconds: 5, resolution: '720p', mode: 'std', sound: 'off', priceMudPoints: 100, referenceImageSrcs: ['data:image/png;base64,image'], referenceVideoSrcs: [ 'generated-character-drafts/editor/seedance-references/video/video.mp4', ], referenceAudioSrcs: [ 'generated-character-drafts/editor/seedance-references/audio/audio.mp3', ], }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/videos/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '参考素材生成视频', model: 'seedance2.0-fast', aspectRatio: '16:9', durationSeconds: 5, resolution: '720p', mode: 'std', sound: 'off', priceMudPoints: 100, referenceImageSrcs: ['data:image/png;base64,image'], referenceVideoSrcs: [ 'generated-character-drafts/editor/seedance-references/video/video.mp4', ], referenceAudioSrcs: [ 'generated-character-drafts/editor/seedance-references/audio/audio.mp3', ], }), }), '生成视频失败', expect.objectContaining({ timeoutMs: 1_200_000 }), ); }); it('generates editor sound effects through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ audioSrc: '/generated-character-drafts/editor-audios/sfx.mp3', width: 420, height: 120, sourceType: 'generated', prompt: '金币掉落叮当声', actualPrompt: '金币掉落叮当声', model: 'audio1.0', provider: 'VectorEngine', taskId: 'sound-task-1', priceMudPoints: 10, audioKind: 'sound-effect', }); const result = await generateEditorSoundEffect({ prompt: '金币掉落叮当声', model: 'audio1.0', duration: 7, priceMudPoints: 10, }); expect(result.taskId).toBe('sound-task-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/audios/sound-effects/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '金币掉落叮当声', model: 'audio1.0', duration: 7, priceMudPoints: 10, }), }), '生成游戏音效失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('sends editor sound effect duration to Vidu instead of legacy type and tempo', async () => { requestJsonMock.mockResolvedValueOnce({ audioSrc: '/generated-character-drafts/editor-audios/sfx-null.mp3', width: 420, height: 120, sourceType: 'generated', prompt: '按钮确认短促音', actualPrompt: '按钮确认短促音', model: 'audio1.0', provider: 'VectorEngine', taskId: 'sound-task-null', priceMudPoints: 10, audioKind: 'sound-effect', }); await generateEditorSoundEffect({ prompt: '按钮确认短促音', model: 'audio1.0', duration: 5, priceMudPoints: 10, }); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/audios/sound-effects/generations', expect.objectContaining({ body: JSON.stringify({ prompt: '按钮确认短促音', model: 'audio1.0', duration: 5, priceMudPoints: 10, }), }), '生成游戏音效失败', expect.any(Object), ); }); it('generates editor background music through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ audioSrc: '/generated-character-drafts/editor-audios/bgm.mp3', width: 420, height: 120, sourceType: 'generated', prompt: '森林冒险背景音乐', actualPrompt: '森林冒险背景音乐', model: 'chirp-v4', provider: 'VectorEngine', taskId: 'music-task-1', priceMudPoints: 5, audioKind: 'background-music', }); const result = await generateEditorBackgroundMusic({ gptDescriptionPrompt: '森林冒险背景音乐', makeInstrumental: true, priceMudPoints: 5, }); expect(result.taskId).toBe('music-task-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/audios/background-music/generations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ gptDescriptionPrompt: '森林冒险背景音乐', makeInstrumental: true, priceMudPoints: 5, }), }), '生成游戏背景音乐失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); it('edits editor images through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ imageSrc: 'data:image/png;base64,edited', width: 1024, height: 1024, sourceType: 'generated', prompt: '把画面改成黄昏光线', actualPrompt: '把画面改成黄昏光线', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'vector-edit-1', }); const result = await editEditorImage({ prompt: '把画面改成黄昏光线', sourceImageSrc: 'data:image/png;base64,source', size: '1024x1024', model: 'gpt-image-2', }); expect(result.taskId).toBe('vector-edit-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/edits', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '把画面改成黄昏光线', sourceImageSrc: 'data:image/png;base64,source', size: '1024x1024', model: 'gpt-image-2', }), }), '修改图片失败', expect.objectContaining({ timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), ); }); });