import { describe, expect, it } from 'vitest'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import { IMAGE_MODEL_GPT_IMAGE_2, IMAGE_MODEL_NANOBANANA2, } from './ImageCanvasGenerationModel'; import { buildCharacterAnimationSubmissionPlan, buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, resolveRegisteredEditorReferenceId, } from './ImageCanvasGenerationSubmissionModel'; function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-source', resourceId: 'resource-source', title: '源图', src: 'data:image/png;base64,source', x: 120, y: 140, width: 320, height: 240, originalWidth: 1024, originalHeight: 768, zIndex: 2, sourceType: 'uploaded', ...overrides, }; } describe('ImageCanvasGenerationSubmissionModel', () => { it('resolves registered image-edit sources by resource ID first', () => { expect( resolveRegisteredEditorReferenceId({ resourceId: ' resource-source ', sourceAssetId: 'asset-source', }), ).toBe('resource-source'); }); it('falls back to a registered asset ID for image edits', () => { expect( resolveRegisteredEditorReferenceId({ resourceId: 'local-resource-source', sourceAssetId: ' asset-source ', }), ).toBe('asset-source'); }); it('rejects unregistered image-edit sources', () => { expect(() => resolveRegisteredEditorReferenceId({ resourceId: 'generation-dialog:source', sourceAssetId: 'upload-source', }), ).toThrow('原图尚未登记为项目资源或素材'); }); it('builds normal image generation submission plans', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'generate', prompt: ' 一张发光主视觉 ', status: 'idle', }, layers: [], nextGeneratedIndex: 3, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: '一张发光主视觉', input: { prompt: '一张发光主视觉', model: 'gemini-3.1-flash-image-preview', style: 'none', aspectRatio: '1:1', imageSize: '1K', }, result: { title: '生成图片 3', generationInputs: { fields: [{ title: '生成提示词', value: '一张发光主视觉' }], references: [], }, }, rememberImageModel: 'gemini-3.1-flash-image-preview', }); }); it('trims custom asset names and limits them to 80 characters', () => { const customName = ` ${'名'.repeat(81)} `; const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'generate', prompt: '一张发光主视觉', assetLabel: customName, status: 'idle', }, layers: [], nextGeneratedIndex: 3, }); expect(plan).toMatchObject({ kind: 'image', result: { title: '名'.repeat(80) }, }); }); it('normalizes legacy nanobanana aliases before submitting image generation', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'generate', prompt: '一张明亮图片', status: 'idle', imageModel: 'nanobanana2', aspectRatio: '16:9', imageSize: '2K', }, layers: [], nextGeneratedIndex: 5, }); expect(plan).toMatchObject({ kind: 'image', input: { model: IMAGE_MODEL_NANOBANANA2, aspectRatio: '16:9', imageSize: '2K', }, rememberImageModel: IMAGE_MODEL_NANOBANANA2, }); }); it('builds edit submission plans with the source layer snapshot', () => { const sourceLayer = createLayer(); const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'edit', prompt: '', status: 'idle', sourceLayerId: sourceLayer.id, }, layers: [sourceLayer], nextGeneratedIndex: 4, }); expect(plan).toMatchObject({ kind: 'edit', normalizedPrompt: '修改当前图片', sourceLayer, generationInputs: { fields: [{ title: '修改要求', value: '修改当前图片' }], references: [ { title: '参考图', label: '源图', refType: 'project-resource', refId: 'resource-source', }, ], }, }); }); it('builds quick-edit canvas generator plans as direct source edits', () => { const sourceLayer = createLayer({ objectKey: 'generated-character-drafts/editor/source.png', assetKind: 'character', originalWidth: 1537, originalHeight: 1025, }); const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'quick-edit', prompt: '把当前图改成雨天', status: 'idle', sourceLayerId: sourceLayer.id, imageModel: IMAGE_MODEL_GPT_IMAGE_2, aspectRatio: '4:3', imageSize: '1K', generationReferences: [ { id: 'style-reference', label: '风格参考', src: 'data:image/png;base64,style', }, ], }, layers: [sourceLayer], nextGeneratedIndex: 4, }); expect(plan).toMatchObject({ kind: 'quick-edit', normalizedPrompt: '把当前图改成雨天', sourceLayer, editInput: { size: '1537x1025', model: IMAGE_MODEL_GPT_IMAGE_2, }, result: { title: '源图 快速编辑', assetKind: 'character', generationInputs: { fields: [{ title: '快速编辑提示词', value: '把当前图改成雨天' }], references: [ { title: '原图', label: '源图', refType: 'project-resource', refId: 'resource-source', }, ], }, }, rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2, }); }); it('does not send image style from an incomplete quick-edit snapshot', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'quick-edit', prompt: '把当前图改成雨天', status: 'idle', style: 'pixelArt', }, layers: [], nextGeneratedIndex: 4, }); expect(plan.kind).toBe('image'); expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty( 'style', ); }); it('builds spec generation plans with reference prompt semantics', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'spec', prompt: '', status: 'idle', specType: 'icon', specValues: { playSetting: '休闲消除', artStyle: '清爽卡通', bodyRatio: '3', characterView: '', customPrompt: '', }, specReference: { id: 'spec-ref', label: '参考.png', src: 'data:image/png;base64,ref', resourceId: 'resource-spec-ref', }, }, layers: [], nextGeneratedIndex: 8, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: 'AI 生成图片', input: { size: '2048x1152', model: 'gpt-image-2', aspectRatio: '16:9', imageSize: '2K', kind: 'spec', referenceImageSrcs: ['data:image/png;base64,ref'], prompt: expect.stringContaining('参考图生成规范'), }, result: { assetKind: 'icon-spec', title: '图标规范 8', }, }); expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual( { fields: [ { title: '玩法设定', value: '休闲消除' }, { title: '美术风格', value: '清爽卡通' }, ], references: [ { title: '参考图', label: '参考.png', refType: 'project-resource', refId: 'resource-spec-ref', }, ], }, ); }); it('treats the renamed UI spec entry as an icon spec asset', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'spec', prompt: '', status: 'idle', specType: 'ui', specValues: { playSetting: '消除类派对玩法', artStyle: '糖果玻璃拟物', bodyRatio: '3', characterView: '', customPrompt: '', }, }, layers: [], nextGeneratedIndex: 2, }); expect(plan).toMatchObject({ kind: 'image', result: { assetKind: 'icon-spec', title: '图标规范 2', }, }); expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty( 'referenceImageSrcs', ); expect(plan.kind === 'image' ? plan.input.prompt : '').not.toContain( '参考图生成规范', ); }); it('builds character plans with references and remembered model', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'character', prompt: ' 白发骑士 ', status: 'idle', imageModel: 'gpt-image-2', style: 'pixelArt', aspectRatio: '2:3', imageSize: '2K', characterSpecReference: { id: 'spec', label: '角色规范', src: 'data:image/png;base64,spec', resourceId: 'resource-character-spec', }, characterReferences: [ { id: 'ref-1', label: '盔甲参考', src: 'data:image/png;base64,armor', resourceId: 'resource-armor', }, ], }, layers: [], nextGeneratedIndex: 9, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: '白发骑士', input: { prompt: '白发骑士', kind: 'character', model: 'gpt-image-2', screenColor: 'auto', segModel: 'birefnet', style: 'pixelArt', aspectRatio: '2:3', imageSize: '2K', referenceImageSrcs: [ 'data:image/png;base64,spec', 'data:image/png;base64,armor', ], }, result: { assetKind: 'character', title: '角色形象 9', }, rememberImageModel: 'gpt-image-2', }); expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual( { fields: [ { title: '角色设定', value: '白发骑士' }, ], references: [ { title: '角色规范', label: '角色规范', refType: 'project-resource', refId: 'resource-character-spec', }, { title: '常规参考图 1', label: '盔甲参考', refType: 'project-resource', refId: 'resource-armor', }, ], }, ); }); it('locks UI design generation submissions to gpt-image-2', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'ui-design', prompt: ' 设计移动端商城首页 ', status: 'idle', imageModel: 'nanobanana2', aspectRatio: '16:9', imageSize: '0.5K', uiDesignSpecReference: { id: 'ui-spec', label: 'UI图标规范', src: 'data:image/png;base64,ui-spec', resourceId: 'resource-ui-spec', }, generationReferences: [ { id: 'ui-ref', label: '界面风格参考', src: 'data:image/png;base64,ui-ref', resourceId: 'resource-ui-ref', }, ], }, layers: [], nextGeneratedIndex: 6, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: '设计移动端商城首页', input: { kind: 'ui-design', model: IMAGE_MODEL_GPT_IMAGE_2, aspectRatio: '16:9', imageSize: '1K', referenceImageSrcs: [ 'data:image/png;base64,ui-spec', 'data:image/png;base64,ui-ref', ], }, result: { assetKind: 'ui-design', title: 'UI设计图 6', generationInputs: { fields: [{ title: '用户输入', value: '设计移动端商城首页' }], references: [ { title: '图标规范', label: 'UI图标规范', refType: 'project-resource', refId: 'resource-ui-spec', }, { title: '参考图 1', label: '界面风格参考', refType: 'project-resource', refId: 'resource-ui-ref', }, ], }, }, rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2, }); }); it('builds publication material plans with structured game fields', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'publication', prompt: '', status: 'idle', style: 'pixelArt', publicationGameInfo: { gameName: ' 重庆洪崖洞火锅 ', gameCategories: '抓大鹅、休闲、治愈、手绘风', gameDescription: '在一堆物品里点击,找到三个一样的物品将其消除', }, publicationReferences: [ { id: 'ref-1', label: '参考图', src: 'data:image/png;base64,ref', resourceId: 'resource-publication-ref', }, ], }, layers: [], nextGeneratedIndex: 10, }); expect(plan).toEqual({ kind: 'image', normalizedPrompt: [ '游戏名:重庆洪崖洞火锅', '游戏分类:抓大鹅、休闲、治愈、手绘风', '一句话描述游戏:在一堆物品里点击,找到三个一样的物品将其消除', ].join('\n'), input: { prompt: expect.stringContaining('【宣发素材类型】游戏首图'), size: '720x540', kind: 'publication-material', model: 'gpt-image-2', aspectRatio: '4:3', imageSize: '1K', referenceImageSrcs: ['data:image/png;base64,ref'], }, result: { assetKind: 'publication-material', title: '10 宣发素材', generationInputs: { fields: [ { title: '游戏名', value: '重庆洪崖洞火锅' }, { title: '游戏分类', value: '抓大鹅、休闲、治愈、手绘风' }, { title: '一句话描述游戏', value: '在一堆物品里点击,找到三个一样的物品将其消除', }, ], references: [ { title: '宣发参考图 1', label: '参考图', refType: 'project-resource', refId: 'resource-publication-ref', }, ], }, }, }); const prompt = plan.kind === 'image' ? plan.input.prompt : ''; expect(prompt).toContain('游戏名:重庆洪崖洞火锅'); expect(prompt).toContain('标题文字必须直接进入画面'); expect(prompt).toContain('输出检查:720 x 540,4:3'); }); it('builds detail gallery publication plans as one 720p portrait image', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'publication', prompt: '', status: 'idle', publicationWorkflowId: 'publication-detail-gallery', publicationGameInfo: { gameName: '云朵消消乐', gameCategories: '', gameDescription: '', }, }, layers: [], nextGeneratedIndex: 11, }); expect(plan).toMatchObject({ kind: 'image', input: { prompt: expect.stringContaining('【宣发素材类型】详情页单图'), size: '720x1280', kind: 'publication-material', aspectRatio: '9:16', imageSize: '1K', }, result: { assetKind: 'publication-material', title: '11 宣发素材', }, }); expect(plan.kind === 'image' ? plan.input.prompt : '').toContain( '本次只生成一张 720 x 1280 竖版详情图', ); expect(plan.kind === 'image' ? plan.input.prompt : '').toContain( '不是五图合集或拼贴', ); }); it('builds promo poster publication plans as 720p landscape images', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'publication', prompt: '', status: 'idle', publicationWorkflowId: 'publication-promo-poster', publicationGameInfo: { gameName: '火锅节活动', gameCategories: '', gameDescription: '', }, }, layers: [], nextGeneratedIndex: 12, }); expect(plan).toMatchObject({ kind: 'image', input: { prompt: expect.stringContaining('【宣发素材类型】运营海报'), size: '1280x720', kind: 'publication-material', aspectRatio: '16:9', imageSize: '1K', }, result: { title: '12 宣发素材', }, }); expect(plan.kind === 'image' ? plan.input.prompt : '').toContain( '16:9 横版 720p,建议 1280 x 720', ); expect(plan.kind === 'image' ? plan.input.prompt : '').toContain( '此处换上你的游戏二维码', ); expect(plan.kind === 'image' ? plan.input.prompt : '').toContain( '方块内部上方用黑色小字', ); }); it('locks publication material plans to gpt-image-2 and does not remember the fixed model', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'publication', prompt: '', status: 'idle', publicationWorkflowId: 'publication-promo-poster', publicationGameInfo: { gameName: '星际塔防', gameCategories: '', gameDescription: '', }, publicationReferences: [], imageModel: 'nanobanana2', }, layers: [], nextGeneratedIndex: 4, }); expect(plan.kind).toBe('image'); if (plan.kind !== 'image') { return; } expect(plan.input).toEqual( expect.objectContaining({ kind: 'publication-material', model: 'gpt-image-2', size: '1280x720', aspectRatio: '16:9', imageSize: '1K', }), ); expect(plan.rememberImageModel).toBeUndefined(); }); it('throws when edit source layer is missing', () => { expect(() => buildImageGenerationSubmissionPlan({ dialog: { mode: 'edit', prompt: '修图', status: 'idle', sourceLayerId: 'missing-layer', }, layers: [], nextGeneratedIndex: 1, }), ).toThrow('未找到要修改的图片'); }); it('returns an icon spritesheet error when the spec reference is missing', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: '', status: 'idle', iconDescriptions: ['返回按钮'], }); expect(plan).toEqual({ ok: false, errorMessage: '请选择图标规范', }); }); it('returns an icon spritesheet error when descriptions are empty', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: '', status: 'idle', iconSpecReference: { id: 'icon-spec', label: '图标规范', src: 'data:image/png;base64,spec', resourceId: 'resource-icon-spec', }, iconDescriptions: [' ', '\n'], }); expect(plan).toEqual({ ok: false, errorMessage: '请填写素材描述', }); }); it('builds icon spritesheet plans with the complete prompt and references', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: ' 返回按钮\n\n设置按钮 ', status: 'idle', imageModel: 'gpt-image-2', style: 'pixelArt', aspectRatio: '3:2', imageSize: '2K', assetLabel: ' 冒险游戏图标 ', iconSpecReference: { id: 'icon-spec', label: '图标规范', src: 'data:image/png;base64,spec', resourceId: 'resource-icon-spec', }, generationReferences: [ { id: 'icon-ref', label: '按钮风格参考', src: 'data:image/png;base64,ref', resourceId: 'resource-icon-ref', }, ], iconDescriptions: ['旧返回', '旧设置'], }); expect(plan).toEqual({ ok: true, iconDescriptions: ['返回按钮\n\n设置按钮'], input: { referenceImageSrc: 'data:image/png;base64,spec', referenceImageSrcs: ['data:image/png;base64,ref'], iconDescriptions: ['返回按钮\n\n设置按钮'], model: 'gpt-image-2', screenColor: 'auto', segModel: 'birefnet', style: 'pixelArt', aspectRatio: '3:2', imageSize: '2K', }, generationInputs: { fields: [{ title: '素材描述', value: '返回按钮\n\n设置按钮' }], references: [ { title: '图标规范', label: '图标规范', refType: 'project-resource', refId: 'resource-icon-spec', }, { title: '参考图 1', label: '按钮风格参考', refType: 'project-resource', refId: 'resource-icon-ref', }, ], }, rememberImageModel: 'gpt-image-2', resultTitle: '冒险游戏图标', }); }); it('builds character animation plans with trimmed prompt and object key source', () => { const sourceLayer = createLayer({ id: 'character-layer', title: '角色图', objectKey: 'generated/character.png', assetKind: 'character', originalWidth: 960, originalHeight: 1280, }); const plan = buildCharacterAnimationSubmissionPlan({ panel: { sourceLayerId: 'character-layer', promptText: ' 循环奔跑动作 ', assetLabel: ' 勇者奔跑 ', resolution: '720p', ratio: 'same', frameCount: 48, durationSeconds: 6, status: 'idle', }, sourceLayer, }); expect(plan).toEqual({ promptText: '循环奔跑动作', resultTitle: '勇者奔跑', input: { sourceLayerId: 'character-layer', sourceImageSrc: 'generated/character.png', sourceWidth: 960, sourceHeight: 1280, promptText: '循环奔跑动作', screenColor: 'auto', resolution: '720p', ratio: 'same', frameCount: 48, durationSeconds: 6, model: 'seedance2.0-fast', assetLabel: '勇者奔跑', }, }); }); it('builds seedance video plans with multimodal references', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: ' 根据参考生成角色宣传片 ', status: 'idle', videoModel: 'seedance2.0-fast', videoResolution: '720p', videoDurationSeconds: 5, generationReferences: [ { id: 'image-ref', label: '参考图.png', src: 'data:image/png;base64,image', mediaType: 'image', mimeType: 'image/png', sizeBytes: 1024, resourceId: 'resource-video-image-ref', }, { id: 'video-ref', label: '参考视频.mp4', src: 'https://signed.example.com/video.mp4', mediaType: 'video', mimeType: 'video/mp4', sizeBytes: 2048, durationSeconds: 4, objectKey: 'generated-character-drafts/editor/seedance-references/video/video.mp4', resourceId: 'resource-video-ref', }, { id: 'audio-ref', label: '参考音频.mp3', src: 'https://signed.example.com/audio.mp3', mediaType: 'audio', mimeType: 'audio/mpeg', sizeBytes: 512, durationSeconds: 3, objectKey: 'generated-character-drafts/editor/seedance-references/audio/audio.mp3', resourceId: 'resource-audio-ref', }, ], }, layers: [], nextGeneratedIndex: 6, }); expect(plan).toMatchObject({ kind: 'video', input: { model: 'seedance2.0-fast', 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', ], }, result: { generationInputs: { fields: [ { title: '视频描述', value: '根据参考生成角色宣传片' }, { title: '时长', value: '5秒' }, ], references: [ { title: '参考图 1', label: '参考图.png', refType: 'project-resource', refId: 'resource-video-image-ref', }, { title: '参考视频 1', label: '参考视频.mp4', refType: 'project-resource', refId: 'resource-video-ref', }, { title: '参考音频 1', label: '参考音频.mp3', refType: 'project-resource', refId: 'resource-audio-ref', }, ], }, }, }); }); it('does not submit references to non seedance video models', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: '镜头推进', status: 'idle', videoModel: 'kling3.0', generationReferences: [ { id: 'image-ref', label: '参考图.png', src: 'data:image/png;base64,image', mediaType: 'image', }, ], }, layers: [], nextGeneratedIndex: 2, }); expect(plan.kind).toBe('video'); if (plan.kind === 'video') { expect(plan.input).not.toHaveProperty('referenceImageSrcs'); expect(plan.input).not.toHaveProperty('referenceVideoSrcs'); expect(plan.input).not.toHaveProperty('referenceAudioSrcs'); } }); it('rejects seedance video plans that only contain audio references', () => { expect(() => buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: '根据音频生成视频', status: 'idle', videoModel: 'seedance2.0', generationReferences: [ { id: 'audio-ref', label: '旁白.wav', src: 'data:audio/wav;base64,audio', mediaType: 'audio', durationSeconds: 4, }, ], }, layers: [], nextGeneratedIndex: 2, }), ).toThrow('参考音频必须搭配参考图片或参考视频'); }); it('rejects seedance video plans that exceed reference count limits', () => { expect(() => buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: '生成视频', status: 'idle', videoModel: 'seedance2.0-fast', generationReferences: Array.from({ length: 10 }, (_, index) => ({ id: `image-${index}`, label: `参考图${index}.png`, src: `data:image/png;base64,${index}`, mediaType: 'image' as const, })), }, layers: [], nextGeneratedIndex: 2, }), ).toThrow('参考图片最多 9 张'); }); it('builds video generation plans with seedance fast as the fallback model', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: ' 角色绕场一周 ', status: 'idle', generationReferences: [], videoResolution: '480p', videoDurationSeconds: 4, }, layers: [], nextGeneratedIndex: 3, }); expect(plan).toMatchObject({ kind: 'video', normalizedPrompt: '角色绕场一周', input: { model: 'seedance2.0-fast', }, result: { title: '生成视频 3', }, }); }); it('builds video generation plans with explicit seedance standard model', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'video', prompt: ' 生成电影感开场动画 ', status: 'idle', videoModel: 'seedance2.0', videoResolution: '720p', videoDurationSeconds: 5, }, layers: [], nextGeneratedIndex: 7, }); expect(plan).toMatchObject({ kind: 'video', normalizedPrompt: '生成电影感开场动画', input: { prompt: '生成电影感开场动画', model: 'seedance2.0', resolution: '720p', durationSeconds: 5, }, result: { title: '生成视频 7', }, }); }); it('builds game sound effect audio submission plans', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'audio-sound-effect', prompt: ' 金币掉落叮当声 ', status: 'idle', soundDurationSeconds: 7, }, layers: [], nextGeneratedIndex: 4, }); expect(plan).toEqual({ kind: 'audio', audioKind: 'sound-effect', normalizedPrompt: '金币掉落叮当声', input: { prompt: '金币掉落叮当声', model: 'audio1.0', duration: 7, }, result: { title: '游戏音效 4', generationInputs: { fields: [ { title: 'prompt', value: '金币掉落叮当声' }, { title: 'model', value: 'audio1.0' }, { title: '时长', value: '7秒' }, ], references: [], }, }, }); }); it('uses the game sound effect fallback for an all-whitespace prompt', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'audio-sound-effect', prompt: ' \t\r\n ', status: 'idle', }, layers: [], nextGeneratedIndex: 6, }); expect(plan).toEqual({ kind: 'audio', audioKind: 'sound-effect', normalizedPrompt: '游戏音效', input: { prompt: '游戏音效', model: 'audio1.0', duration: 5, }, result: { title: '游戏音效 6', generationInputs: { fields: [ { title: 'prompt', value: '游戏音效' }, { title: 'model', value: 'audio1.0' }, { title: '时长', value: '5秒' }, ], references: [], }, }, }); }); it('builds game background music audio submission plans with instrumental fixed to true', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'audio-background-music', prompt: '不会进入正式请求的旧值', status: 'idle', }, layers: [], nextGeneratedIndex: 5, canonicalBackgroundMusicPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF', }); expect(plan).toEqual({ kind: 'audio', audioKind: 'background-music', normalizedPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF', input: { gptDescriptionPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF', makeInstrumental: true, }, result: { title: '游戏背景音乐 5', generationInputs: { fields: [ { title: 'gpt_description_prompt', value: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF', }, ], references: [], }, }, }); }); it('does not build a background music plan without a claimed canonical prompt', () => { expect(() => buildImageGenerationSubmissionPlan({ dialog: { mode: 'audio-background-music', prompt: ' 不应原生 trim 或回退默认值 ', status: 'idle', }, layers: [], nextGeneratedIndex: 1, }), ).toThrow('背景音乐提交缺少已确认的提示词'); }); });