import { describe, expect, test } from 'vitest'; import type { PuzzleLeaderboardEntry, PuzzleRunSnapshot, PuzzleRuntimeLevelSnapshot, } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import { mergePuzzleServiceRuntimeState } from './platformPuzzleRuntimeStateModel'; const currentLeaderboard: PuzzleLeaderboardEntry[] = [ { rank: 1, nickname: '本地玩家', elapsedMs: 12000, isCurrentPlayer: true, }, ]; const serviceLevelLeaderboard: PuzzleLeaderboardEntry[] = [ { rank: 1, nickname: '服务端玩家', elapsedMs: 9000, }, ]; const serviceRunLeaderboard: PuzzleLeaderboardEntry[] = [ { rank: 2, nickname: '全局玩家', elapsedMs: 15000, }, ]; function buildPuzzleLevel( overrides: Partial = {}, ): PuzzleRuntimeLevelSnapshot { return { runId: 'run-current', levelIndex: 0, levelId: 'level-1', gridSize: 3, profileId: 'puzzle-profile-current', levelName: '星桥机关', authorDisplayName: '玩家', themeTags: ['星桥'], coverImageSrc: '/cover.png', board: { rows: 3, cols: 3, pieces: [], mergedGroups: [], selectedPieceId: null, allTilesResolved: true, }, status: 'cleared', startedAtMs: 1000, clearedAtMs: 13000, elapsedMs: 12000, timeLimitMs: 120000, remainingMs: 108000, pausedAccumulatedMs: 0, pauseStartedAtMs: null, freezeAccumulatedMs: 0, freezeStartedAtMs: null, freezeUntilMs: null, leaderboardEntries: currentLeaderboard, ...overrides, }; } function buildPuzzleRun( overrides: Partial = {}, ): PuzzleRunSnapshot { return { runId: 'run-current', entryProfileId: 'puzzle-profile-current', clearedLevelCount: 1, currentLevelIndex: 0, currentGridSize: 3, playedProfileIds: ['puzzle-profile-current'], previousLevelTags: ['星桥'], currentLevel: buildPuzzleLevel(), recommendedNextProfileId: null, nextLevelMode: 'sameWork', nextLevelProfileId: null, nextLevelId: null, recommendedNextWorks: [], leaderboardEntries: currentLeaderboard, ...overrides, }; } describe('platformPuzzleRuntimeStateModel', () => { test('keeps current run when either current level is missing', () => { const currentRun = buildPuzzleRun({ currentLevel: null }); expect( mergePuzzleServiceRuntimeState(currentRun, buildPuzzleRun()), ).toBe(currentRun); const serviceRun = buildPuzzleRun({ currentLevel: null }); const playableCurrentRun = buildPuzzleRun(); expect( mergePuzzleServiceRuntimeState(playableCurrentRun, serviceRun), ).toBe(playableCurrentRun); }); test('merges service leaderboard and next-level handoff without replacing local level state', () => { const currentRun = buildPuzzleRun({ clearedLevelCount: 2, currentLevel: buildPuzzleLevel({ runId: 'run-current', status: 'cleared', board: { rows: 3, cols: 3, pieces: [ { pieceId: 'piece-local', correctRow: 0, correctCol: 0, currentRow: 0, currentCol: 0, mergedGroupId: null, }, ], mergedGroups: [], selectedPieceId: 'piece-local', allTilesResolved: true, }, }), }); const serviceRun = buildPuzzleRun({ runId: 'run-service', entryProfileId: 'puzzle-profile-service', clearedLevelCount: 1, recommendedNextProfileId: 'next-recommended', nextLevelMode: 'similarWorks', nextLevelProfileId: 'next-profile', nextLevelId: 'next-level', recommendedNextWorks: [ { profileId: 'next-profile', levelName: '月桥机关', authorDisplayName: '推荐作者', themeTags: ['月桥'], coverImageSrc: '/next-cover.png', similarityScore: 0.91, }, ], currentLevel: buildPuzzleLevel({ runId: 'run-service-level', status: 'playing', leaderboardEntries: serviceLevelLeaderboard, }), }); const merged = mergePuzzleServiceRuntimeState(currentRun, serviceRun); expect(merged.runId).toBe('run-service'); expect(merged.entryProfileId).toBe('puzzle-profile-service'); expect(merged.clearedLevelCount).toBe(2); expect(merged.recommendedNextProfileId).toBe('next-recommended'); expect(merged.nextLevelMode).toBe('similarWorks'); expect(merged.nextLevelProfileId).toBe('next-profile'); expect(merged.nextLevelId).toBe('next-level'); expect(merged.recommendedNextWorks).toEqual(serviceRun.recommendedNextWorks); expect(merged.leaderboardEntries).toEqual(serviceLevelLeaderboard); expect(merged.currentLevel?.status).toBe('cleared'); expect(merged.currentLevel?.board.pieces).toEqual( currentRun.currentLevel?.board.pieces, ); expect(merged.currentLevel?.leaderboardEntries).toEqual( serviceLevelLeaderboard, ); }); test('falls back to service run leaderboard, then current level leaderboard', () => { const currentRun = buildPuzzleRun(); const serviceRun = buildPuzzleRun({ currentLevel: buildPuzzleLevel({ leaderboardEntries: [] }), leaderboardEntries: serviceRunLeaderboard, }); expect( mergePuzzleServiceRuntimeState(currentRun, serviceRun).currentLevel ?.leaderboardEntries, ).toEqual(serviceRunLeaderboard); expect( mergePuzzleServiceRuntimeState(currentRun, { ...serviceRun, leaderboardEntries: [], }).currentLevel?.leaderboardEntries, ).toEqual(currentLeaderboard); }); });