import { describe, expect, test } from 'vitest'; import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation'; import { buildPlatformRecommendedEntries } from './platformRecommendation'; const NOW_MS = Date.parse('2026-06-07T12:00:00.000Z'); type PublicCardTestParams = { id: string; sourceType?: 'puzzle' | 'match3d' | 'jump-hop'; subtitle?: string; summaryText?: string; coverImageSrc?: string | null; themeTags?: string[]; playCount?: number; remixCount?: number; likeCount?: number; recentPlayCount7d?: number; publishedAt?: string | null; updatedAt?: string; }; function buildPublicCard( params: PublicCardTestParams, ): PlatformPublicGalleryCard { const sourceType = params.sourceType ?? 'puzzle'; return { sourceType, workId: `${sourceType}-work-${params.id}`, profileId: `${sourceType}-profile-${params.id}`, publicWorkCode: `${sourceType.toUpperCase()}-${params.id}`, ownerUserId: `user-${params.id}`, authorDisplayName: `${params.id} 作者`, worldName: `${params.id} 作品`, subtitle: params.subtitle ?? '公开作品', summaryText: params.summaryText ?? '公开作品摘要。', coverImageSrc: params.coverImageSrc ?? `${params.id}.png`, themeTags: params.themeTags ?? ['推荐'], playCount: params.playCount ?? 0, remixCount: params.remixCount ?? 0, likeCount: params.likeCount ?? 0, recentPlayCount7d: params.recentPlayCount7d ?? 0, visibility: 'published', publishedAt: params.publishedAt ?? '2026-06-01T12:00:00.000Z', updatedAt: params.updatedAt ?? params.publishedAt ?? '2026-06-01T12:00:00.000Z', } satisfies PlatformPublicGalleryCard; } describe('buildPlatformRecommendedEntries', () => { test('combines heat, freshness and featured boost after de-duplicating works', () => { const coldEntry = buildPublicCard({ id: 'cold', playCount: 1, publishedAt: '2026-04-01T12:00:00.000Z', }); const hotRecentEntry = buildPublicCard({ id: 'hot', playCount: 8, likeCount: 4, recentPlayCount7d: 16, publishedAt: '2026-06-06T12:00:00.000Z', }); const curatedEntry = buildPublicCard({ id: 'curated', playCount: 0, likeCount: 0, publishedAt: '2026-05-10T12:00:00.000Z', }); const entries = buildPlatformRecommendedEntries( { featuredEntries: [curatedEntry], latestEntries: [coldEntry, hotRecentEntry, curatedEntry], }, { nowMs: NOW_MS }, ); expect(entries.map((entry) => entry.profileId)).toEqual([ hotRecentEntry.profileId, curatedEntry.profileId, coldEntry.profileId, ]); }); test('interleaves close-score works from different play types', () => { const firstPuzzle = buildPublicCard({ id: 'puzzle-a', sourceType: 'puzzle', likeCount: 2, }); const secondPuzzle = buildPublicCard({ id: 'puzzle-b', sourceType: 'puzzle', likeCount: 2, }); const match3d = buildPublicCard({ id: 'match3d-a', sourceType: 'match3d', likeCount: 2, }); const entries = buildPlatformRecommendedEntries( { featuredEntries: [], latestEntries: [firstPuzzle, secondPuzzle, match3d], }, { nowMs: NOW_MS }, ); expect(entries.map((entry) => entry.profileId)).toEqual([ firstPuzzle.profileId, match3d.profileId, secondPuzzle.profileId, ]); }); test('separates same-type candidates while alternatives remain', () => { const hotPuzzle = buildPublicCard({ id: 'hot-puzzle', sourceType: 'puzzle', recentPlayCount7d: 50, likeCount: 20, }); const warmPuzzle = buildPublicCard({ id: 'warm-puzzle', sourceType: 'puzzle', recentPlayCount7d: 32, likeCount: 12, }); const coldMatch3d = buildPublicCard({ id: 'cold-match3d', sourceType: 'match3d', publishedAt: '2026-04-01T12:00:00.000Z', }); const entries = buildPlatformRecommendedEntries( { featuredEntries: [], latestEntries: [hotPuzzle, warmPuzzle, coldMatch3d], }, { nowMs: NOW_MS }, ); expect(entries.map((entry) => entry.profileId)).toEqual([ hotPuzzle.profileId, coldMatch3d.profileId, warmPuzzle.profileId, ]); }); test('falls back to same-type adjacency when no other type remains', () => { const firstPuzzle = buildPublicCard({ id: 'only-puzzle-a', sourceType: 'puzzle', recentPlayCount7d: 8, }); const secondPuzzle = buildPublicCard({ id: 'only-puzzle-b', sourceType: 'puzzle', recentPlayCount7d: 4, }); const entries = buildPlatformRecommendedEntries( { featuredEntries: [], latestEntries: [firstPuzzle, secondPuzzle], }, { nowMs: NOW_MS }, ); expect(entries.map((entry) => entry.profileId)).toEqual([ firstPuzzle.profileId, secondPuzzle.profileId, ]); }); });