304 lines
8.2 KiB
TypeScript
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);
|
|
});
|
|
});
|