469 lines
14 KiB
TypeScript
469 lines
14 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([
|
|
'编译首关草稿',
|
|
'生成关卡名称',
|
|
'并行生成素材',
|
|
'校验背景资源',
|
|
'写入正式草稿',
|
|
]);
|
|
expect(progress?.phaseLabel).toBe('编译首关草稿');
|
|
expect(progress?.steps[0]?.detail).toBe(
|
|
'读取画面描述,建立可编辑草稿与首关结构。',
|
|
);
|
|
expect(progress?.estimatedRemainingMs).toBe(298_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, 282_000);
|
|
const writeBackProgress = buildMiniGameDraftGenerationProgress(
|
|
state,
|
|
296_000,
|
|
);
|
|
|
|
expect(imageProgress?.phaseId).toBe('puzzle-images');
|
|
expect(imageProgress?.estimatedRemainingMs).toBe(275_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-background');
|
|
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
|
|
expect(writeBackProgress?.estimatedRemainingMs).toBe(5_000);
|
|
expect(writeBackProgress?.steps[3]?.status).toBe('completed');
|
|
expect(writeBackProgress?.steps[4]?.status).toBe('active');
|
|
});
|
|
|
|
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, 360_000);
|
|
|
|
expect(progress?.phaseId).toBe('puzzle-select-image');
|
|
expect(progress?.overallProgress).toBe(98);
|
|
expect(progress?.estimatedRemainingMs).toBe(0);
|
|
expect(progress?.steps[4]?.completed).toBe(1);
|
|
});
|
|
|
|
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 item sheet and image 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-background-prompt',
|
|
'match3d-material-sheet',
|
|
'match3d-slice-images',
|
|
'match3d-upload-images',
|
|
'match3d-generate-views',
|
|
'match3d-background-image',
|
|
'match3d-write-draft',
|
|
]);
|
|
expect(progress?.phaseId).toBe('match3d-material-sheet');
|
|
expect(progress?.phaseLabel).toBe('分批生成素材图');
|
|
expect(progress?.estimatedRemainingMs).toBe(480_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-generate-views');
|
|
expect(progress?.steps[6]?.detail).toContain('五视角图片');
|
|
expect(progress?.steps[6]?.completed).toBe(1);
|
|
expect(progress?.steps[6]?.total).toBe(3);
|
|
});
|
|
|
|
test('match3d draft generation reaches background image and writeback phases', () => {
|
|
const state = createMiniGameDraftGenerationState('match3d');
|
|
|
|
const backgroundProgress = buildMiniGameDraftGenerationProgress(
|
|
state,
|
|
state.startedAtMs + 400_000,
|
|
);
|
|
const writeProgress = buildMiniGameDraftGenerationProgress(
|
|
state,
|
|
state.startedAtMs + 500_000,
|
|
);
|
|
|
|
expect(backgroundProgress?.phaseId).toBe('match3d-background-image');
|
|
expect(backgroundProgress?.phaseLabel).toBe('生成UI背景');
|
|
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: '25 件',
|
|
},
|
|
]);
|
|
});
|
|
|
|
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: '一只猫在雨夜灯牌下回头。',
|
|
},
|
|
]);
|
|
});
|
|
});
|