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 { 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 { 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); }); });