Files
Genarrative/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts

304 lines
8.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import type { PuzzleAnchorPack } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { MiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
import {
createFailedMiniGameDraftGenerationStateForRestoredDraft,
createMiniGameDraftGenerationStateForRestoredDraft,
createPuzzleDraftGenerationStateFromPayload,
isMiniGameDraftGenerating,
isMiniGameDraftReady,
mergePuzzleSessionProgressIntoGenerationState,
rebaseMiniGameDraftBackgroundCompileTaskForDisplay,
rebaseMiniGameDraftGenerationStateForDisplay,
resolveFinishedMiniGameDraftGenerationState,
resolvePuzzlePhaseFromSessionProgress,
} from './platformMiniGameDraftGenerationStateModel';
const NOW = Date.parse('2026-06-04T03:00:00.000Z');
const SESSION_UPDATED_AT = '2026-06-01T10:00:00.000Z';
const SESSION_UPDATED_AT_MS = Date.parse(SESSION_UPDATED_AT);
function buildAnchorPack(): PuzzleAnchorPack {
const item = {
key: 'theme',
label: '主题',
value: '星桥机关',
status: 'confirmed' as const,
};
return {
themePromise: item,
visualSubject: item,
visualMood: item,
compositionHooks: item,
tagsAndForbidden: item,
};
}
function buildPuzzleSession(
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
const anchorPack = buildAnchorPack();
return {
sessionId: 'puzzle-session-1',
seedText: '星桥',
currentTurn: 1,
progressPercent: 90,
stage: 'draft_ready',
anchorPack,
draft: {
workTitle: '星桥拼图',
workDescription: '修复星桥机关。',
levelName: '星桥机关',
summary: '把星桥碎片拼回原位。',
themeTags: ['星桥'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'generating',
levels: [],
},
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: null,
updatedAt: SESSION_UPDATED_AT,
...overrides,
};
}
function buildState(
overrides: Partial<MiniGameDraftGenerationState> = {},
): MiniGameDraftGenerationState {
return {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 100,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
metadata: {
puzzleAiRedraw: true,
puzzleActivePhaseId: 'compile',
puzzleActiveStepStartedAtMs: 200,
puzzleProgressPercent: 20,
},
...overrides,
};
}
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(NOW);
});
afterEach(() => {
vi.useRealTimers();
});
describe('platformMiniGameDraftGenerationStateModel', () => {
test('creates restored generation state with metadata and explicit start time', () => {
expect(
createMiniGameDraftGenerationStateForRestoredDraft(
'match3d',
{ puzzleAiRedraw: false },
123,
),
).toMatchObject({
kind: 'match3d',
phase: 'match3d-work-title',
startedAtMs: 123,
metadata: {
puzzleAiRedraw: false,
},
});
});
test('creates failed restored state from backend updated time', () => {
expect(
createFailedMiniGameDraftGenerationStateForRestoredDraft(
'puzzle',
SESSION_UPDATED_AT,
'生成失败',
{ puzzleAiRedraw: true },
),
).toMatchObject({
kind: 'puzzle',
phase: 'failed',
startedAtMs: SESSION_UPDATED_AT_MS,
finishedAtMs: NOW,
error: '生成失败',
metadata: {
puzzleAiRedraw: true,
},
});
});
test('rebases finished state for display without changing other fields', () => {
const state = buildState({
phase: 'ready',
finishedAtMs: 300,
completedAssetCount: 2,
totalAssetCount: 3,
});
expect(rebaseMiniGameDraftGenerationStateForDisplay(state)).toEqual({
...state,
finishedAtMs: undefined,
});
expect(
rebaseMiniGameDraftBackgroundCompileTaskForDisplay({
sessionId: 'task-1',
generationState: state,
}),
).toEqual({
sessionId: 'task-1',
generationState: {
...state,
finishedAtMs: undefined,
},
});
});
test('creates puzzle generation state from payload and compiled session', () => {
const payload: CreatePuzzleAgentSessionRequest = {
seedText: '星桥',
aiRedraw: false,
};
expect(createPuzzleDraftGenerationStateFromPayload(payload)).toMatchObject({
kind: 'puzzle',
phase: 'compile',
startedAtMs: NOW,
metadata: {
puzzleAiRedraw: false,
puzzleActivePhaseId: undefined,
puzzleActiveStepStartedAtMs: undefined,
puzzleProgressPercent: undefined,
},
});
expect(
createPuzzleDraftGenerationStateFromPayload(payload, buildPuzzleSession()),
).toMatchObject({
kind: 'puzzle',
phase: 'compile',
startedAtMs: SESSION_UPDATED_AT_MS,
metadata: {
puzzleAiRedraw: false,
puzzleActivePhaseId: 'compile',
puzzleActiveStepStartedAtMs: NOW,
puzzleProgressPercent: 90,
},
});
});
test('resolves puzzle phase from backend progress thresholds', () => {
const state = buildState();
expect(
resolvePuzzlePhaseFromSessionProgress(
state,
buildPuzzleSession({ progressPercent: 96 }),
),
).toBe('puzzle-select-image');
expect(
resolvePuzzlePhaseFromSessionProgress(
state,
buildPuzzleSession({ progressPercent: 94 }),
),
).toBe('puzzle-ui-assets');
expect(
resolvePuzzlePhaseFromSessionProgress(
buildState({ metadata: { puzzleAiRedraw: false } }),
buildPuzzleSession({ progressPercent: 88 }),
),
).toBe('puzzle-level-scene');
expect(
resolvePuzzlePhaseFromSessionProgress(
state,
buildPuzzleSession({ progressPercent: 88 }),
),
).toBe('puzzle-cover-image');
expect(
resolvePuzzlePhaseFromSessionProgress(
state,
buildPuzzleSession({ progressPercent: 20 }),
),
).toBe('compile');
});
test('merges compiled puzzle session progress into generation state', () => {
expect(
mergePuzzleSessionProgressIntoGenerationState(
buildState({
metadata: {
puzzleAiRedraw: false,
puzzleActivePhaseId: 'compile',
puzzleActiveStepStartedAtMs: 200,
puzzleProgressPercent: 20,
},
}),
buildPuzzleSession({ progressPercent: 90 }),
),
).toMatchObject({
metadata: {
puzzleAiRedraw: false,
puzzleActivePhaseId: 'puzzle-level-scene',
puzzleActiveStepStartedAtMs: SESSION_UPDATED_AT_MS,
puzzleProgressPercent: 90,
},
});
expect(
mergePuzzleSessionProgressIntoGenerationState(
buildState(),
buildPuzzleSession({
draft: {
...buildPuzzleSession().draft!,
formDraft: {
pictureDescription: '星桥',
},
},
}),
).metadata,
).toMatchObject({
puzzleActivePhaseId: 'compile',
puzzleActiveStepStartedAtMs: 200,
puzzleProgressPercent: 20,
});
});
test('finishes generation state and resolves ready/generating flags', () => {
const failedState = resolveFinishedMiniGameDraftGenerationState(
buildState({ error: '旧错误' }),
'failed',
{
completedAssetCount: 1,
totalAssetCount: 2,
},
);
expect(failedState).toMatchObject({
phase: 'failed',
finishedAtMs: NOW,
error: '旧错误',
completedAssetCount: 1,
totalAssetCount: 2,
});
expect(isMiniGameDraftReady(failedState)).toBe(false);
expect(isMiniGameDraftGenerating(failedState)).toBe(false);
expect(isMiniGameDraftReady({ ...failedState, phase: 'ready' })).toBe(true);
expect(isMiniGameDraftGenerating(buildState())).toBe(true);
expect(isMiniGameDraftGenerating(null)).toBe(false);
});
});