Files
Genarrative/src/services/miniGameDraftGenerationProgress.test.ts
2026-05-22 16:09:01 +08:00

520 lines
16 KiB
TypeScript

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?.estimatedRemainingMs).toBe(296_500);
expect(progress?.overallProgress).toBeGreaterThan(0);
expect(progress?.steps[0]?.completed).toBeGreaterThan(0);
});
test('puzzle draft generation advances steps across the current asset pipeline', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const imageProgress = buildMiniGameDraftGenerationProgress(state, 26_000);
const uiProgress = buildMiniGameDraftGenerationProgress(state, 206_000);
const writeBackProgress = buildMiniGameDraftGenerationProgress(
state,
296_000,
);
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
expect(imageProgress?.estimatedRemainingMs).toBe(273_000);
expect(imageProgress?.steps[1]?.status).toBe('completed');
expect(imageProgress?.steps[2]?.status).toBe('active');
expect(imageProgress?.steps[2]?.completed).toBeGreaterThan(0);
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
expect(writeBackProgress?.steps[4]?.status).toBe('completed');
expect(writeBackProgress?.steps[5]?.status).toBe('active');
});
test('puzzle write-back step turns completed once rounded progress reaches 100%', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1_000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 298_950);
expect(progress?.phaseId).toBe('puzzle-select-image');
expect(progress?.overallProgress).toBe(98);
expect(progress?.estimatedRemainingMs).toBe(50);
expect(progress?.steps[5]?.completed).toBe(1);
expect(progress?.steps[5]?.status).toBe('completed');
});
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('puzzle-level-scene');
expect(progress?.steps[2]?.detail).toContain('直接使用上传图作为参考');
expect(progress?.estimatedRemainingMs).toBe(189_000);
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
});
test('puzzle draft generation keeps moving without claiming completion before response', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 480_000);
expect(progress?.phaseId).toBe('puzzle-select-image');
expect(progress?.overallProgress).toBe(98);
expect(progress?.estimatedRemainingMs).toBe(0);
expect(progress?.steps[5]?.completed).toBe(1);
expect(progress?.steps[5]?.status).toBe('completed');
expect(progress?.steps.every((step) => step.status === 'completed')).toBe(
true,
);
});
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 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 sound 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-hit-sound',
'wooden-fish-write-draft',
]);
expect(progress?.phaseId).toBe('wooden-fish-hit-object');
expect(progress?.phaseLabel).toBe('生成敲击物图案');
expect(progress?.estimatedRemainingMs).toBe(272_000);
});
test('wooden fish generation anchors expose hit object, sound and words', () => {
const entries = buildWoodenFishGenerationAnchorEntries(null, {
templateId: 'wooden-fish',
workTitle: '每日一敲',
workDescription: '敲一下,好事发生。',
themeTags: ['解压'],
hitObjectPrompt: '金色小木鱼',
hitSoundPrompt: '清脆木鱼声',
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: '一只猫在雨夜灯牌下回头。',
},
]);
});
});