import { describe, expect, test } from 'vitest'; import { buildBabyObjectMatchGenerationAnchorEntries, buildJumpHopGenerationAnchorEntries, buildMatch3DGenerationAnchorEntries, buildMiniGameDraftGenerationProgress, buildPuzzleGenerationAnchorEntries, buildWoodenFishGenerationAnchorEntries, createMiniGameDraftGenerationState, type MiniGameDraftGenerationState, } from './miniGameDraftGenerationProgress'; describe('miniGameDraftGenerationProgress', () => { test('puzzle draft generation follows picture-only creation steps', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 2500); expect(progress?.steps.map((step) => step.label)).toEqual([ '编译首关草稿', '生成关卡名称', '生成拼图首图', '生成关卡画面', '生成UI与背景', '写入正式草稿', ]); expect(progress?.phaseLabel).toBe('编译首关草稿'); expect(progress?.steps[0]?.detail).toBe( '建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。', ); expect(progress?.steps[2]?.detail).toBe('生成 1:1 拼图首图,预计 4 分钟。'); expect(progress?.estimatedRemainingMs).toBe(446_500); expect(progress?.overallProgress).toBe(0); expect(progress?.steps[0]?.completed).toBeGreaterThan(0); }); test('puzzle draft generation starts total progress from zero', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 1000); expect(progress?.overallProgress).toBe(0); expect(progress?.completedWeight).toBe(0); expect(progress?.estimatedRemainingMs).toBe(448_000); expect(progress?.steps[0]?.completed).toBe(0); }); test('puzzle draft generation total progress advances after startup', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 130_000); expect(progress?.overallProgress).toBeGreaterThan(0); expect(progress?.overallProgress).toBeLessThan(88); expect(progress?.phaseId).toBe('compile'); }); test('puzzle draft generation keeps current step until real progress advances it', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const longRunningProgress = buildMiniGameDraftGenerationProgress( state, 296_000, ); const progressedState: MiniGameDraftGenerationState = { ...state, phase: 'puzzle-cover-image', }; const realProgress = buildMiniGameDraftGenerationProgress( progressedState, 296_000, ); expect(longRunningProgress?.phaseId).toBe('compile'); expect(longRunningProgress?.steps[0]?.status).toBe('active'); expect(longRunningProgress?.steps[1]?.status).toBe('pending'); expect(longRunningProgress?.overallProgress).toBeLessThan(5); expect(longRunningProgress?.overallProgress).toBeGreaterThan(0); expect(realProgress?.phaseId).toBe('puzzle-cover-image'); expect(realProgress?.steps[1]?.status).toBe('completed'); expect(realProgress?.steps[2]?.status).toBe('active'); }); test('puzzle write-back step stays active until the generation action finishes', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'puzzle-select-image', startedAtMs: 1_000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 448_950); expect(progress?.phaseId).toBe('puzzle-select-image'); expect(progress?.overallProgress).toBe(98); expect(progress?.estimatedRemainingMs).toBe(50); expect(progress?.steps[5]?.completed).toBe(0.98); expect(progress?.steps[5]?.status).toBe('active'); }); test('puzzle direct upload generation skips the first image generation step', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1_000, completedAssetCount: 0, totalAssetCount: 0, error: null, metadata: { puzzleAiRedraw: false, }, }; const progress = buildMiniGameDraftGenerationProgress(state, 20_000); const writeBackProgress = buildMiniGameDraftGenerationProgress( state, 206_000, ); expect(progress?.steps.map((step) => step.label)).toEqual([ '编译首关草稿', '生成关卡名称', '生成关卡画面', '生成UI与背景', '写入正式草稿', ]); expect(progress?.phaseId).toBe('compile'); expect(progress?.steps[2]?.detail).toContain('直接使用上传图作为参考'); expect(progress?.estimatedRemainingMs).toBe(189_000); expect(writeBackProgress?.phaseId).toBe('compile'); expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000); }); test('puzzle draft generation does not advance or claim completion before response', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 630_000); expect(progress?.phaseId).toBe('compile'); expect(progress?.overallProgress).toBeLessThan(88); expect(progress?.overallProgress).toBeLessThan(5); expect(progress?.overallProgress).toBeGreaterThan(0); expect(progress?.estimatedRemainingMs).toBe(0); expect(progress?.steps[0]?.status).toBe('active'); expect(progress?.steps[0]?.completed).toBe(0.98); expect( progress?.steps.slice(1).every((step) => step.status === 'pending'), ).toBe(true); }); test('puzzle draft generation advances steps from backend progress percent only', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1_000, completedAssetCount: 0, totalAssetCount: 0, error: null, metadata: { puzzleAiRedraw: true, puzzleProgressPercent: 88, } as MiniGameDraftGenerationState['metadata'], }; const imageProgress = buildMiniGameDraftGenerationProgress(state, 120_000); const uiProgress = buildMiniGameDraftGenerationProgress( { ...state, metadata: { puzzleAiRedraw: true, puzzleProgressPercent: 94, } as MiniGameDraftGenerationState['metadata'], }, 230_000, ); const writeProgress = buildMiniGameDraftGenerationProgress( { ...state, metadata: { puzzleAiRedraw: true, puzzleProgressPercent: 96, } as MiniGameDraftGenerationState['metadata'], }, 260_000, ); expect(imageProgress?.phaseId).toBe('puzzle-cover-image'); expect(imageProgress?.overallProgress).toBeLessThan(88); expect(imageProgress?.steps[2]?.status).toBe('active'); expect(imageProgress?.steps[3]?.status).toBe('pending'); expect(uiProgress?.phaseId).toBe('puzzle-ui-assets'); expect(uiProgress?.overallProgress).toBeLessThan(94); expect(uiProgress?.steps[4]?.status).toBe('active'); expect(uiProgress?.steps[5]?.status).toBe('pending'); expect(writeProgress?.phaseId).toBe('puzzle-select-image'); expect(writeProgress?.steps[5]?.status).toBe('active'); }); test('puzzle backend milestone starts fake progress from the current step entry time', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'compile', startedAtMs: 1_000, completedAssetCount: 0, totalAssetCount: 0, error: null, metadata: { puzzleAiRedraw: true, puzzleProgressPercent: 88, puzzleActivePhaseId: 'puzzle-cover-image', puzzleActiveStepStartedAtMs: 120_000, } as MiniGameDraftGenerationState['metadata'], }; const progress = buildMiniGameDraftGenerationProgress(state, 121_000); expect(progress?.phaseId).toBe('puzzle-cover-image'); expect(progress?.overallProgress).toBeLessThan(88); expect(progress?.steps[2]?.status).toBe('active'); expect(progress?.steps[2]?.completed).toBeLessThan(0.02); }); test('puzzle ready copy points to result page work info completion', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'ready', startedAtMs: 1000, completedAssetCount: 1, totalAssetCount: 1, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 2000); expect(progress?.phaseDetail).toBe( '首关草稿与正式图已准备完成,可进入结果页补作品信息。', ); }); test('finished draft generation keeps elapsed time pinned to completion time', () => { const state: MiniGameDraftGenerationState = { kind: 'puzzle', phase: 'failed', startedAtMs: 1_000, finishedAtMs: 151_000, completedAssetCount: 0, totalAssetCount: 0, error: 'VectorEngine 图片生成请求超时', }; const progress = buildMiniGameDraftGenerationProgress(state, 500_000); expect(progress?.elapsedMs).toBe(150_000); }); test('big fish draft generation exposes multiple draft steps', () => { const state: MiniGameDraftGenerationState = { kind: 'big-fish', phase: 'big-fish-draft', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 1500); expect(progress).not.toBeNull(); expect(progress?.steps).toHaveLength(3); expect(progress?.steps.map((step) => step.id)).toEqual([ 'big-fish-draft', 'big-fish-levels', 'big-fish-runtime', ]); expect(progress?.steps[0]?.label).toBe('整理玩法骨架'); }); test('big fish generation progresses to level and runtime phases over time', () => { const state: MiniGameDraftGenerationState = { kind: 'big-fish', phase: 'big-fish-draft', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const levelProgress = buildMiniGameDraftGenerationProgress(state, 3200); const runtimeProgress = buildMiniGameDraftGenerationProgress(state, 6200); expect(levelProgress?.phaseId).toBe('big-fish-levels'); expect(levelProgress?.phaseLabel).toBe('编译等级蓝图'); expect(runtimeProgress?.phaseId).toBe('big-fish-runtime'); expect(runtimeProgress?.phaseLabel).toBe('校准场地与参数'); }); test('big fish ready copy directs user to continue generating assets on result page', () => { const state: MiniGameDraftGenerationState = { kind: 'big-fish', phase: 'ready', startedAtMs: 1000, completedAssetCount: 0, totalAssetCount: 0, error: null, }; const progress = buildMiniGameDraftGenerationProgress(state, 2000); expect(progress?.phaseDetail).toBe( '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。', ); }); test('match3d draft generation exposes level scene derived asset steps', () => { const state = createMiniGameDraftGenerationState('match3d'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 30_000, ); expect(progress?.steps.map((step) => step.id)).toEqual([ 'match3d-work-title', 'match3d-item-names', 'match3d-level-scene', 'match3d-derived-assets', 'match3d-parse-spritesheet', 'match3d-write-draft', ]); expect(progress?.phaseId).toBe('match3d-level-scene'); expect(progress?.phaseLabel).toBe('生成关卡整图'); expect(progress?.estimatedRemainingMs).toBe(430_000); }); test('match3d draft generation starts from title generation', () => { const state = createMiniGameDraftGenerationState('match3d'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 1_000, ); expect(progress?.phaseId).toBe('match3d-work-title'); expect(progress?.phaseLabel).toBe('建立草稿存档'); expect(progress?.steps[0]?.detail).toBe( '创建可恢复作品草稿,锁定本次题材和难度。', ); }); test('match3d draft generation starts total progress from zero', () => { const state = createMiniGameDraftGenerationState('match3d'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs, ); expect(progress?.overallProgress).toBe(0); expect(progress?.completedWeight).toBe(0); expect(progress?.steps[0]?.completed).toBe(0); }); test('match3d draft generation keeps backend observed asset phase', () => { const state = { ...createMiniGameDraftGenerationState('match3d'), phase: 'match3d-generate-views' as const, completedAssetCount: 1, totalAssetCount: 3, }; const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 20_000, ); expect(progress?.phaseId).toBe('match3d-parse-spritesheet'); expect(progress?.steps[4]?.detail).toContain('解析 20 个物品'); expect(progress?.steps[4]?.completed).toBe(1); expect(progress?.steps[4]?.total).toBe(3); }); test('match3d draft generation reaches background image and writeback phases', () => { const state = createMiniGameDraftGenerationState('match3d'); const derivedProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 150_000, ); const writeProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 460_000, ); expect(derivedProgress?.phaseId).toBe('match3d-derived-assets'); expect(derivedProgress?.phaseLabel).toBe('生成三张派生图'); expect(writeProgress?.phaseId).toBe('match3d-write-draft'); expect(writeProgress?.phaseLabel).toBe('写入草稿页'); }); test('match3d generation anchors show theme and difficulty item count', () => { const entries = buildMatch3DGenerationAnchorEntries(null, { themeText: '水果', clearCount: 20, difficulty: 8, referenceImageSrc: null, }); expect(entries).toEqual([ { id: 'match3d-theme', label: '题材', value: '水果', }, { id: 'match3d-items', label: '素材数量', value: '20 种素材', }, ]); }); test('baby object match generation exposes two item names', () => { const state = createMiniGameDraftGenerationState('baby-object-match'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 9_000, ); const entries = buildBabyObjectMatchGenerationAnchorEntries({ itemAName: '苹果', itemBName: '香蕉', }); expect(progress?.steps.map((step) => step.id)).toEqual([ 'baby-object-draft', 'baby-object-images', 'baby-object-ready', ]); expect(progress?.phaseId).toBe('baby-object-images'); expect(progress?.estimatedRemainingMs).toBe(351_000); expect(entries).toEqual([ { id: 'baby-object-item-1', label: '物品 1', value: '苹果', }, { id: 'baby-object-item-2', label: '物品 2', value: '香蕉', }, ]); }); test('jump hop draft generation exposes character and tile atlas pipeline', () => { const state = createMiniGameDraftGenerationState('jump-hop'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 35_000, ); expect(progress?.steps.map((step) => step.id)).toEqual([ 'jump-hop-draft', 'jump-hop-character', 'jump-hop-tile-atlas', 'jump-hop-slice-tiles', 'jump-hop-write-draft', ]); expect(progress?.phaseId).toBe('jump-hop-character'); expect(progress?.phaseLabel).toBe('生成角色形象'); expect(progress?.estimatedRemainingMs).toBe(265_000); }); test('jump hop generation anchors expose theme, character and tile style', () => { const entries = buildJumpHopGenerationAnchorEntries(null, { themeText: '云端糖果塔', characterDescription: '披着星星披风的小旅人', tileStyle: '纸模玩具', difficulty: '标准', rhythmPreference: '轻快', }); expect(entries).toEqual([ { id: 'jump-hop-theme', label: '主题', value: '云端糖果塔', }, { id: 'jump-hop-character', label: '角色', value: '披着星星披风的小旅人', }, { id: 'jump-hop-tile-style', label: '地块', value: '纸模玩具', }, ]); }); test('wooden fish draft generation exposes hit object, background and back button pipeline', () => { const state = createMiniGameDraftGenerationState('wooden-fish'); const progress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 28_000, ); expect(progress?.steps.map((step) => step.id)).toEqual([ 'wooden-fish-draft', 'wooden-fish-hit-object', 'wooden-fish-background', 'wooden-fish-back-button', 'wooden-fish-write-draft', ]); expect(progress?.phaseId).toBe('wooden-fish-hit-object'); expect(progress?.phaseLabel).toBe('生成敲击物图案'); expect(progress?.estimatedRemainingMs).toBe(530_000); }); test('wooden fish draft generation follows hit object, background, back button and writeback', () => { const state = createMiniGameDraftGenerationState('wooden-fish'); const hitObjectProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 20_000, ); const backgroundProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 200_000, ); const backButtonProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 390_000, ); const writeBackProgress = buildMiniGameDraftGenerationProgress( state, state.startedAtMs + 575_000, ); expect(hitObjectProgress?.phaseId).toBe('wooden-fish-hit-object'); expect(backgroundProgress?.phaseId).toBe('wooden-fish-background'); expect(backButtonProgress?.phaseId).toBe('wooden-fish-back-button'); expect(writeBackProgress?.phaseId).toBe('wooden-fish-write-draft'); expect(writeBackProgress?.estimatedRemainingMs).toBe(0); expect(writeBackProgress?.steps[4]?.status).toBe('completed'); }); test('wooden fish generation anchors expose hit object, sound and words', () => { const entries = buildWoodenFishGenerationAnchorEntries(null, { templateId: 'wooden-fish', workTitle: '每日一敲', workDescription: '敲一下,好事发生。', themeTags: ['解压'], hitObjectPrompt: '金色小木鱼', hitSoundAsset: { assetId: 'wooden-fish-default-hit-sound', audioSrc: '/wooden-fish/default-hit-sound.mp3', audioObjectKey: 'public/wooden-fish/default-hit-sound.mp3', assetObjectId: 'wooden-fish-default-hit-sound', source: 'bundled-default', prompt: '默认木鱼音', durationMs: 3000, }, floatingWords: ['幸运+1', '功德+1'], }); expect(entries).toEqual([ { id: 'wooden-fish-hit-object', label: '敲击物', value: '金色小木鱼', }, { id: 'wooden-fish-hit-sound', label: '音效', value: '默认木鱼音', }, { id: 'wooden-fish-words', label: '飘字', value: '幸运+1、功德+1', }, ]); }); test('puzzle generation anchors expose form payload as the display source', () => { const entries = buildPuzzleGenerationAnchorEntries( { sessionId: 'puzzle-session-1', currentTurn: 1, progressPercent: 0, stage: 'collecting_anchors', anchorPack: { themePromise: { key: 'themePromise', label: '题材承诺', value: '雨夜猫街', status: 'locked', }, visualSubject: { key: 'visualSubject', label: '画面主体', value: '一只猫在雨夜灯牌下回头。', status: 'locked', }, visualMood: { key: 'visualMood', label: '视觉气质', value: '清晰、适合拼图切块', status: 'inferred', }, compositionHooks: { key: 'compositionHooks', label: '拼图记忆点', value: '主体轮廓、色块分区、局部细节', status: 'inferred', }, tagsAndForbidden: { key: 'tagsAndForbidden', label: '标签与禁忌', value: '猫咪、雨夜、拼图;禁止标题字', status: 'inferred', }, }, draft: null, messages: [], lastAssistantReply: null, publishedProfileId: null, suggestedActions: [], resultPreview: null, updatedAt: '2026-04-29T00:00:00.000Z', }, { seedText: '一只猫在雨夜灯牌下回头。', pictureDescription: '一只猫在雨夜灯牌下回头。', referenceImageSrc: null, }, ); expect(entries).toEqual([ { id: 'picture-description', label: '画面描述', value: '一只猫在雨夜灯牌下回头。', }, ]); }); });