import { expect, test } from 'vitest'; import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime'; import { EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME, type PlatformPublicGalleryCard, } from '../rpg-entry/rpgEntryWorldPresentation'; import { getPlatformPublicGalleryEntryKey, getPlatformPublicGalleryEntryTime, getPlatformRecommendRuntimeKind, isPlatformRecommendRuntimeReadyForEntry, isSamePlatformPublicGalleryEntry, mergePlatformPublicGalleryEntries, type PlatformRecommendRuntimeStartIntentDeps, type RecommendRuntimeKind, resolvePlatformRecommendRuntimeStartIntent, } from './platformPublicGalleryFlow'; import { mapBarkBattlePublicDetailToWorkSummary, mapPublicWorkDetailToBigFishWork, mapPublicWorkDetailToPuzzleWork, mapPublicWorkDetailToSquareHoleWork, } from './platformPublicWorkDetailFlow'; type TypedPlatformPublicGalleryCard = Extract< PlatformPublicGalleryCard, { sourceType: string } >; type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType']; type TypedPlatformPublicGalleryCardOverrides = Partial< Omit >; function buildRpgEntry( overrides: Partial = {}, ): CustomWorldGalleryCard { return { ownerUserId: 'user-1', profileId: 'rpg-profile', publicWorkCode: 'CW-RPG', authorPublicUserCode: null, visibility: 'published', publishedAt: '2026-06-01T00:00:00.000Z', updatedAt: '2026-06-01T01:00:00.000Z', authorDisplayName: '玩家', worldName: 'RPG 世界', subtitle: '公开作品', summaryText: '公开作品摘要', coverImageSrc: null, themeMode: 'martial', playableNpcCount: 1, landmarkCount: 1, ...overrides, }; } function buildTypedEntry( sourceType: PlatformGallerySourceType, overrides: TypedPlatformPublicGalleryCardOverrides = {}, ): PlatformPublicGalleryCard { const common = { workId: `${sourceType}-work`, profileId: `${sourceType}-profile`, publicWorkCode: `${sourceType}-code`, ownerUserId: 'user-1', authorDisplayName: '玩家', worldName: `${sourceType} 作品`, subtitle: '公开作品', summaryText: '公开作品摘要', coverImageSrc: null, themeTags: [sourceType], visibility: 'published' as const, publishedAt: '2026-06-01T00:00:00.000Z', updatedAt: '2026-06-01T01:00:00.000Z', }; switch (sourceType) { case 'puzzle': return { ...common, ...overrides, sourceType }; case 'big-fish': return { ...common, ...overrides, sourceType }; case 'match3d': return { ...common, ...overrides, sourceType }; case 'square-hole': return { ...common, ...overrides, sourceType }; case 'visual-novel': return { ...common, ...overrides, sourceType }; case 'jump-hop': return { ...common, ...overrides, sourceType }; case 'wooden-fish': return { ...common, ...overrides, sourceType }; case 'edutainment': return { ...common, ...overrides, sourceType, templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME, }; case 'bark-battle': return { ...common, ...overrides, sourceType, authorPublicUserCode: null, coverRenderMode: 'image', coverCharacterImageSrcs: [], themeMode: 'martial', playableNpcCount: 1, landmarkCount: 1, }; default: { const exhaustive: never = sourceType; return exhaustive; } } } function buildPuzzleWork( overrides: Partial = {}, ): PuzzleWorkSummary { return { workId: 'puzzle-work', profileId: 'puzzle-profile', ownerUserId: 'user-1', sourceSessionId: 'puzzle-session', authorDisplayName: '玩家', levelName: '拼图作品', summary: '拼图摘要', themeTags: ['拼图'], coverImageSrc: '/puzzle-cover.png', publicationStatus: 'published', updatedAt: '2026-06-01T01:00:00.000Z', publishedAt: '2026-06-01T00:00:00.000Z', playCount: 3, remixCount: 2, likeCount: 1, pointIncentiveTotalHalfPoints: 0, pointIncentiveClaimedPoints: 0, pointIncentiveTotalPoints: 0, pointIncentiveClaimablePoints: 0, publishReady: true, ...overrides, }; } function buildMatch3DWork( overrides: Partial = {}, ): Match3DWorkSummary { return { workId: 'match3d-work', profileId: 'match3d-profile', ownerUserId: 'user-1', sourceSessionId: 'match3d-session', gameName: '抓大鹅作品', themeText: '经典消除', summary: '抓大鹅摘要', tags: ['抓大鹅'], coverImageSrc: '/match3d-cover.png', referenceImageSrc: null, clearCount: 12, difficulty: 4, publicationStatus: 'published', playCount: 10, updatedAt: '2026-06-01T01:00:00.000Z', publishedAt: '2026-06-01T00:00:00.000Z', publishReady: true, generatedItemAssets: [], ...overrides, }; } function buildBarkBattleWork( overrides: Partial = {}, ): BarkBattleWorkSummary { return { workId: 'bark-battle-work', draftId: 'bark-battle-draft', ownerUserId: 'user-1', authorDisplayName: '玩家', title: '汪汪声浪作品', summary: '汪汪摘要', themeDescription: '森林擂台', playerImageDescription: '小狗', opponentImageDescription: '对手', playerCharacterImageSrc: '/player.png', opponentCharacterImageSrc: '/opponent.png', uiBackgroundImageSrc: '/bark-bg.png', difficultyPreset: 'normal', status: 'published', generationStatus: 'ready', publishReady: true, playCount: 9, recentPlayCount7d: 2, updatedAt: '2026-06-01T01:00:00.000Z', publishedAt: '2026-06-01T00:00:00.000Z', ...overrides, }; } function buildRecommendRuntimeStartDeps( overrides: Partial = {}, ): PlatformRecommendRuntimeStartIntentDeps { return { selectedPuzzleDetail: null, barkBattleGalleryEntries: [], mapMatch3DWork: () => buildMatch3DWork(), ...overrides, }; } test('platform public gallery flow resolves stable key and runtime kind for every play kind', () => { const cases: Array< [sourceType: PlatformGallerySourceType, keyKind: string, kind: RecommendRuntimeKind] > = [ ['big-fish', 'big-fish', 'big-fish'], ['puzzle', 'puzzle', 'puzzle'], ['jump-hop', 'jump-hop', 'jump-hop'], ['wooden-fish', 'wooden-fish', 'wooden-fish'], ['match3d', 'match3d', 'match3d'], ['square-hole', 'square-hole', 'square-hole'], ['visual-novel', 'visual-novel', 'visual-novel'], ['bark-battle', 'bark-battle', 'bark-battle'], [ 'edutainment', `edutainment:${EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID}`, 'edutainment', ], ]; cases.forEach(([sourceType, keyKind, kind]) => { const entry = buildTypedEntry(sourceType); expect(getPlatformPublicGalleryEntryKey(entry)).toBe( `${keyKind}:user-1:${sourceType}-profile`, ); expect(getPlatformRecommendRuntimeKind(entry)).toBe(kind); }); const rpgEntry = buildRpgEntry(); expect(getPlatformPublicGalleryEntryKey(rpgEntry)).toBe( 'rpg:user-1:rpg-profile', ); expect(getPlatformRecommendRuntimeKind(rpgEntry)).toBe('rpg'); }); test('platform public gallery flow compares entries by resolved identity', () => { const left = buildTypedEntry('puzzle'); const sameIdentity = buildTypedEntry('puzzle', { workId: 'other-work', worldName: '新标题', }); const otherKind = buildTypedEntry('match3d', { ownerUserId: left.ownerUserId, profileId: left.profileId, }); expect(isSamePlatformPublicGalleryEntry(left, sameIdentity)).toBe(true); expect(isSamePlatformPublicGalleryEntry(left, otherKind)).toBe(false); }); test('platform public gallery flow resolves recommend runtime start intent', () => { const bigFishEntry = buildTypedEntry('big-fish'); expect( resolvePlatformRecommendRuntimeStartIntent( bigFishEntry, buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-big-fish', work: mapPublicWorkDetailToBigFishWork(bigFishEntry), returnStage: 'platform', embedded: true, }); const selectedPuzzleDetail = buildPuzzleWork({ profileId: 'puzzle-profile', }); expect( resolvePlatformRecommendRuntimeStartIntent( buildTypedEntry('puzzle'), buildRecommendRuntimeStartDeps({ selectedPuzzleDetail }), ), ).toEqual({ type: 'start-puzzle', work: selectedPuzzleDetail, returnStage: 'platform', embedded: true, }); const puzzleEntry = buildTypedEntry('puzzle', { profileId: 'fallback-puzzle-profile', }); expect( resolvePlatformRecommendRuntimeStartIntent( puzzleEntry, buildRecommendRuntimeStartDeps({ selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }), }), ), ).toEqual({ type: 'start-puzzle', work: mapPublicWorkDetailToPuzzleWork(puzzleEntry), returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( buildTypedEntry('jump-hop'), buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-jump-hop', profileId: 'jump-hop-profile', returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( buildTypedEntry('wooden-fish'), buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-wooden-fish', profileId: 'wooden-fish-profile', returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( buildTypedEntry('visual-novel'), buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-visual-novel', profileId: 'visual-novel-profile', returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( buildTypedEntry('edutainment'), buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-edutainment', entry: buildTypedEntry('edutainment'), returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( buildRpgEntry(), buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'mark-ready', }); }); test('platform public gallery flow resolves recommend runtime mapper-backed start intent', () => { const match3DEntry = buildTypedEntry('match3d'); const match3DWork = buildMatch3DWork({ workId: 'mapped-match3d-work' }); expect( resolvePlatformRecommendRuntimeStartIntent( match3DEntry, buildRecommendRuntimeStartDeps({ mapMatch3DWork: (entry) => entry === match3DEntry ? match3DWork : null, }), ), ).toEqual({ type: 'start-match3d', work: match3DWork, returnStage: 'work-detail', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( match3DEntry, buildRecommendRuntimeStartDeps({ mapMatch3DWork: () => null }), ), ).toEqual({ type: 'blocked', errorTarget: 'match3d', errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。', }); const squareHoleEntry = buildTypedEntry('square-hole'); expect( resolvePlatformRecommendRuntimeStartIntent( squareHoleEntry, buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-square-hole', work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry), returnStage: 'platform', embedded: true, }); }); test('platform public gallery flow resolves recommend runtime bark battle priority', () => { const entry = buildTypedEntry('bark-battle'); const galleryWork = buildBarkBattleWork({ workId: 'bark-battle-work', title: '推荐缓存', }); expect( resolvePlatformRecommendRuntimeStartIntent( entry, buildRecommendRuntimeStartDeps({ barkBattleGalleryEntries: [galleryWork], }), ), ).toEqual({ type: 'start-bark-battle', work: galleryWork, returnStage: 'platform', embedded: true, }); expect( resolvePlatformRecommendRuntimeStartIntent( entry, buildRecommendRuntimeStartDeps(), ), ).toEqual({ type: 'start-bark-battle', work: mapBarkBattlePublicDetailToWorkSummary(entry), returnStage: 'platform', embedded: true, }); }); test('platform public gallery flow resolves recommend runtime readiness', () => { expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), { activeKind: 'puzzle', hasBigFishRun: true, }), ).toBe(false); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), { activeKind: 'big-fish', hasBigFishRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('jump-hop'), { activeKind: 'jump-hop', hasJumpHopRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('wooden-fish'), { activeKind: 'wooden-fish', hasWoodenFishRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('match3d'), { activeKind: 'match3d', hasMatch3DRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('square-hole'), { activeKind: 'square-hole', hasSquareHoleRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('visual-novel'), { activeKind: 'visual-novel', hasVisualNovelRun: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('bark-battle'), { activeKind: 'bark-battle', }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildRpgEntry(), { activeKind: 'rpg', }), ).toBe(true); }); test('platform public gallery flow resolves puzzle and edutainment readiness details', () => { const puzzleEntry = buildTypedEntry('puzzle', { profileId: 'puzzle-profile', }); expect( isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, { activeKind: 'puzzle', puzzleRunEntryProfileId: 'other-profile', puzzleRunCurrentLevelProfileId: 'puzzle-profile', }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, { activeKind: 'puzzle', puzzleRunEntryProfileId: 'other-profile', puzzleRunCurrentLevelProfileId: 'another-profile', }), ).toBe(false); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), { activeKind: 'edutainment', hasBabyObjectMatchDraft: true, }), ).toBe(true); expect( isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), { activeKind: 'edutainment', hasBabyObjectMatchDraft: false, }), ).toBe(false); }); test('platform public gallery flow merges duplicate identities and sorts newest first', () => { const staleRpgEntry = buildRpgEntry({ profileId: 'shared-rpg', worldName: '旧版 RPG', publishedAt: '2026-06-01T00:00:00.000Z', }); const freshRpgEntry = buildRpgEntry({ profileId: 'shared-rpg', worldName: '新版 RPG', publishedAt: '2026-06-04T00:00:00.000Z', }); const middleRpgEntry = buildRpgEntry({ profileId: 'middle-rpg', worldName: '中间 RPG', publishedAt: '2026-06-02T00:00:00.000Z', }); const updatedOnlyEntry = buildTypedEntry('big-fish', { profileId: 'updated-only', publishedAt: null, updatedAt: '2026-06-03T00:00:00.000Z', }); const invalidTimeEntry = buildTypedEntry('puzzle', { profileId: 'invalid-time', publishedAt: 'not-a-date', updatedAt: 'still-not-a-date', }); const merged = mergePlatformPublicGalleryEntries( [staleRpgEntry, middleRpgEntry], [invalidTimeEntry, updatedOnlyEntry, freshRpgEntry], ); expect(merged).toHaveLength(4); expect(merged.map((entry) => entry.profileId)).toEqual([ 'shared-rpg', 'updated-only', 'middle-rpg', 'invalid-time', ]); expect(merged[0]?.worldName).toBe('新版 RPG'); expect(getPlatformPublicGalleryEntryTime(invalidTimeEntry)).toBe(0); });