From 7219057f0c37b82c741976ef5082c1edbb6b0628 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Wed, 6 May 2026 18:03:21 +0200 Subject: [PATCH 1/2] refactor: simplify recommendations runtime --- .../server/HomeRecommendationServices.kt | 11 +- .../dev/typetype/server/ServiceRegistry.kt | 30 ++---- .../dev/typetype/server/db/DatabaseFactory.kt | 17 --- .../server/db/DatabaseIndexMigrations.kt | 6 -- .../server/db/tables/SettingsTable.kt | 1 - .../typetype/server/models/SettingsItem.kt | 1 - .../typetype/server/routes/UserDataRoutes.kt | 4 - .../server/services/BlockedService.kt | 4 +- .../server/services/FavoritesService.kt | 16 +-- .../server/services/HistoryService.kt | 17 +-- .../services/HomeRecommendationBuilder.kt | 66 +----------- .../HomeRecommendationCandidateService.kt | 15 --- .../HomeRecommendationDiscoveryAssembler.kt | 22 +--- .../services/HomeRecommendationPageBuilder.kt | 7 -- .../services/HomeRecommendationPoolBuilder.kt | 37 +------ .../HomeRecommendationPoolResolver.kt | 18 +--- ...eRecommendationPoolResolverDependencies.kt | 6 -- .../services/HomeRecommendationScoring.kt | 59 +++------- .../services/HomeRecommendationService.kt | 6 -- .../HomeRecommendationUserSignalService.kt | 101 ++---------------- .../services/RecommendationEventService.kt | 18 ---- .../services/RecommendationFeedbackService.kt | 11 -- .../services/RecommendationPrivacyService.kt | 5 +- .../server/services/SettingsService.kt | 3 - .../SubscriptionShortsBlendService.kt | 12 +-- .../services/SubscriptionShortsFeedService.kt | 2 +- .../SubscriptionShortsSignalService.kt | 22 +--- .../server/services/WatchLaterService.kt | 16 +-- .../YoutubeTakeoutPreferenceService.kt | 11 +- 29 files changed, 59 insertions(+), 485 deletions(-) diff --git a/src/main/kotlin/dev/typetype/server/HomeRecommendationServices.kt b/src/main/kotlin/dev/typetype/server/HomeRecommendationServices.kt index 86f583e..9046ee1 100644 --- a/src/main/kotlin/dev/typetype/server/HomeRecommendationServices.kt +++ b/src/main/kotlin/dev/typetype/server/HomeRecommendationServices.kt @@ -4,25 +4,18 @@ import dev.typetype.server.cache.DragonflyService import dev.typetype.server.services.HomeRecommendationPoolResolver import dev.typetype.server.services.HomeRecommendationPoolResolverDependencies import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationPrivacyService data class HomeRecommendationServices( - val feedHistoryService: RecommendationFeedHistoryService, val recommendationService: HomeRecommendationService, ) fun createHomeRecommendationServices( cache: DragonflyService, deps: HomeRecommendationPoolResolverDependencies, - privacyService: RecommendationPrivacyService, ): HomeRecommendationServices { - val feedHistoryService = RecommendationFeedHistoryService() - val resolverDeps = deps.copy(cache = cache, feedHistoryService = feedHistoryService) + val resolverDeps = deps.copy(cache = cache) val recommendationService = HomeRecommendationService( poolResolver = HomeRecommendationPoolResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) - return HomeRecommendationServices(feedHistoryService, recommendationService) + return HomeRecommendationServices(recommendationService) } diff --git a/src/main/kotlin/dev/typetype/server/ServiceRegistry.kt b/src/main/kotlin/dev/typetype/server/ServiceRegistry.kt index 8274daf..d75ec8a 100644 --- a/src/main/kotlin/dev/typetype/server/ServiceRegistry.kt +++ b/src/main/kotlin/dev/typetype/server/ServiceRegistry.kt @@ -31,11 +31,6 @@ import dev.typetype.server.services.PipePipeSuggestionService import dev.typetype.server.services.PipePipeTrendingService import dev.typetype.server.services.PlaylistService import dev.typetype.server.services.ProgressService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.RecommendationOnboardingService -import dev.typetype.server.services.RecommendationPrivacyService import dev.typetype.server.services.HomeRecommendationSignalContextService import dev.typetype.server.services.SearchHistoryService import dev.typetype.server.services.SettingsService @@ -43,7 +38,6 @@ import dev.typetype.server.services.HomeRecommendationPoolResolverDependencies import dev.typetype.server.services.SubscriptionFeedService import dev.typetype.server.services.SubscriptionShortsBlendService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.SubscriptionShortsSignalService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.SubscriptionFeedCacheInvalidation import dev.typetype.server.services.SubscriptionFeedCacheInvalidator @@ -71,29 +65,24 @@ internal class ServiceRegistry(cache: DragonflyService, subtitleServiceUrl: Stri val nativeManifestService = CachedNativeManifestService(NativeManifestService(), cache) val hlsManifestService = HlsManifestService(streamService, proxyHttpClient) val suggestionService = CachedSuggestionService(PipePipeSuggestionService(), cache) - val recommendationPrivacyService = RecommendationPrivacyService(SettingsService()) - val recommendationInterestService = RecommendationInterestService() - val recommendationEventService = RecommendationEventService(recommendationInterestService, recommendationPrivacyService) - val historyService = HistoryService(recommendationEventService) + val historyService = HistoryService() val subscriptionsService = SubscriptionsService() val subscriptionFeedService = SubscriptionFeedService(subscriptionsService, channelService, cache) val subscriptionShortsFeedService = SubscriptionShortsFeedService( subscriptionsService, channelService, - SubscriptionShortsBlendService(trendingService, SubscriptionShortsSignalService(recommendationEventService)), + SubscriptionShortsBlendService(trendingService), cache, ) val notificationsService = NotificationsService(subscriptionFeedService) val playlistService = PlaylistService() - val watchLaterService = WatchLaterService(recommendationEventService) + val watchLaterService = WatchLaterService() val progressService = ProgressService() - val favoritesService = FavoritesService(recommendationEventService) + val favoritesService = FavoritesService() val settingsService = SettingsService() val searchHistoryService = SearchHistoryService() - val blockedService = BlockedService(recommendationEventService) + val blockedService = BlockedService() val bugReportService = BugReportService() - val recommendationFeedbackService = RecommendationFeedbackService(recommendationEventService) - val recommendationOnboardingService = RecommendationOnboardingService() val recommendationSignalContextService = HomeRecommendationSignalContextService(subscriptionsService, historyService, favoritesService) val youtubeTakeoutImportService = YoutubeTakeoutFactory.create(subscriptionsService, playlistService, historyService, favoritesService, watchLaterService) val recommendationPoolResolverDependencies = HomeRecommendationPoolResolverDependencies( @@ -101,20 +90,13 @@ internal class ServiceRegistry(cache: DragonflyService, subtitleServiceUrl: Stri subscriptionFeedService = subscriptionFeedService, subscriptionShortsFeedService = subscriptionShortsFeedService, historyService = historyService, - playlistService = playlistService, favoritesService = favoritesService, watchLaterService = watchLaterService, blockedService = blockedService, - feedbackService = recommendationFeedbackService, - eventService = recommendationEventService, - feedHistoryService = dev.typetype.server.services.RecommendationFeedHistoryService(), signalContextService = recommendationSignalContextService, - trendingService = trendingService, - searchService = searchService, streamService = streamService, cache = cache, ) - private val homeRecommendationServices = createHomeRecommendationServices(cache, recommendationPoolResolverDependencies, recommendationPrivacyService) - val recommendationFeedHistoryService = homeRecommendationServices.feedHistoryService + private val homeRecommendationServices = createHomeRecommendationServices(cache, recommendationPoolResolverDependencies) val homeRecommendationService = homeRecommendationServices.recommendationService } diff --git a/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt b/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt index af16b6b..a3b98af 100644 --- a/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt +++ b/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt @@ -19,13 +19,6 @@ import dev.typetype.server.db.tables.WatchLaterTable import dev.typetype.server.db.tables.AdminSettingsTable import dev.typetype.server.db.tables.PasswordResetTable import dev.typetype.server.db.tables.NotificationStatesTable -import dev.typetype.server.db.tables.RecommendationEventsTable -import dev.typetype.server.db.tables.RecommendationFeedbackTable -import dev.typetype.server.db.tables.RecommendationFeedHistoryTable -import dev.typetype.server.db.tables.RecommendationOnboardingPreferencesTable -import dev.typetype.server.db.tables.RecommendationOnboardingStateTable -import dev.typetype.server.db.tables.UserChannelInterestTable -import dev.typetype.server.db.tables.UserTopicInterestTable import dev.typetype.server.db.tables.YoutubeTakeoutImportJobsTable import dev.typetype.server.db.tables.YoutubeTakeoutPlaylistKeysTable import kotlinx.coroutines.Dispatchers @@ -64,13 +57,6 @@ object DatabaseFactory { BlockedChannelsTable, BlockedVideosTable, PasswordResetTable, - RecommendationFeedbackTable, - RecommendationEventsTable, - RecommendationFeedHistoryTable, - RecommendationOnboardingStateTable, - RecommendationOnboardingPreferencesTable, - UserChannelInterestTable, - UserTopicInterestTable, YoutubeTakeoutImportJobsTable, YoutubeTakeoutPlaylistKeysTable, BugReportsTable, @@ -82,7 +68,6 @@ object DatabaseFactory { exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS default_subtitle_language TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS default_audio_language TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS prefer_original_language BOOLEAN NOT NULL DEFAULT false") - exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS recommendation_personalization_enabled BOOLEAN NOT NULL DEFAULT true") exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS subscription_sync_interval INTEGER NOT NULL DEFAULT 0") exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS channel_avatar TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") @@ -103,8 +88,6 @@ object DatabaseFactory { exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_code TEXT") exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS public_username TEXT") exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT") - exec("ALTER TABLE recommendation_events ADD COLUMN IF NOT EXISTS watch_duration_ms BIGINT") - exec("ALTER TABLE recommendation_events ADD COLUMN IF NOT EXISTS context_key TEXT") exec("ALTER TABLE youtube_takeout_import_jobs ADD COLUMN IF NOT EXISTS preview_json TEXT") exec("ALTER TABLE bug_reports ALTER COLUMN github_issue_url TYPE TEXT") DatabaseSessionAuthMigration.apply() diff --git a/src/main/kotlin/dev/typetype/server/db/DatabaseIndexMigrations.kt b/src/main/kotlin/dev/typetype/server/db/DatabaseIndexMigrations.kt index 5e8353a..6c10747 100644 --- a/src/main/kotlin/dev/typetype/server/db/DatabaseIndexMigrations.kt +++ b/src/main/kotlin/dev/typetype/server/db/DatabaseIndexMigrations.kt @@ -12,12 +12,6 @@ object DatabaseIndexMigrations { exec("CREATE INDEX IF NOT EXISTS idx_watch_later_user_added_at ON watch_later (user_id, added_at DESC)") exec("CREATE INDEX IF NOT EXISTS idx_search_history_user_searched_at ON search_history (user_id, searched_at DESC)") exec("CREATE INDEX IF NOT EXISTS idx_playlists_user_created_at ON playlists (user_id, created_at DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_reco_feedback_user_created_at ON recommendation_feedback (user_id, created_at DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_reco_events_user_event_occurred ON recommendation_events (user_id, event_type, occurred_at DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_reco_events_user_occurred ON recommendation_events (user_id, occurred_at DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_reco_feed_history_user_last_shown ON recommendation_feed_history (user_id, last_shown DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_user_channel_interest_user_score ON user_channel_interest (user_id, score DESC)") - exec("CREATE INDEX IF NOT EXISTS idx_user_topic_interest_user_score ON user_topic_interest (user_id, score DESC)") exec("CREATE INDEX IF NOT EXISTS idx_history_title_trgm ON history USING gin (lower(title) gin_trgm_ops)") exec("CREATE INDEX IF NOT EXISTS idx_history_channel_name_trgm ON history USING gin (lower(channel_name) gin_trgm_ops)") } diff --git a/src/main/kotlin/dev/typetype/server/db/tables/SettingsTable.kt b/src/main/kotlin/dev/typetype/server/db/tables/SettingsTable.kt index 100fba1..f6b29fd 100644 --- a/src/main/kotlin/dev/typetype/server/db/tables/SettingsTable.kt +++ b/src/main/kotlin/dev/typetype/server/db/tables/SettingsTable.kt @@ -13,6 +13,5 @@ object SettingsTable : Table("settings") { val defaultSubtitleLanguage = text("default_subtitle_language").default("") val defaultAudioLanguage = text("default_audio_language").default("") val preferOriginalLanguage = bool("prefer_original_language").default(false) - val recommendationPersonalizationEnabled = bool("recommendation_personalization_enabled").default(true) override val primaryKey = PrimaryKey(userId) } diff --git a/src/main/kotlin/dev/typetype/server/models/SettingsItem.kt b/src/main/kotlin/dev/typetype/server/models/SettingsItem.kt index 71a2045..baddab9 100644 --- a/src/main/kotlin/dev/typetype/server/models/SettingsItem.kt +++ b/src/main/kotlin/dev/typetype/server/models/SettingsItem.kt @@ -13,5 +13,4 @@ data class SettingsItem( val defaultSubtitleLanguage: String = "", val defaultAudioLanguage: String = "", val preferOriginalLanguage: Boolean = false, - val recommendationPersonalizationEnabled: Boolean = true, ) diff --git a/src/main/kotlin/dev/typetype/server/routes/UserDataRoutes.kt b/src/main/kotlin/dev/typetype/server/routes/UserDataRoutes.kt index a6f227b..a4fdacb 100644 --- a/src/main/kotlin/dev/typetype/server/routes/UserDataRoutes.kt +++ b/src/main/kotlin/dev/typetype/server/routes/UserDataRoutes.kt @@ -27,9 +27,6 @@ internal fun Route.userDataRoutes( settingsRoutes(svc.settingsService, authService) searchHistoryRoutes(svc.searchHistoryService, authService) blockedRoutes(svc.blockedService, authService) - recommendationEventsRoutes(svc.recommendationEventService, authService) - recommendationFeedbackRoutes(svc.recommendationFeedbackService, authService) - recommendationOnboardingRoutes(svc.recommendationOnboardingService, authService) notificationsRoutes(svc.notificationsService, authService) youtubeTakeoutImportRoutes(svc.youtubeTakeoutImportService, authService) profileRoutes(profileService, avatarService, authService) @@ -37,5 +34,4 @@ internal fun Route.userDataRoutes( restoreRoutes(restoreService, authService) homeRecommendationRoutes(svc.homeRecommendationService, authService) homeRecommendationShortsRoutes(svc.homeRecommendationService, authService) - homeRecommendationMetricsRoutes(svc.homeRecommendationService, authService) } diff --git a/src/main/kotlin/dev/typetype/server/services/BlockedService.kt b/src/main/kotlin/dev/typetype/server/services/BlockedService.kt index d4534c1..9528d2a 100644 --- a/src/main/kotlin/dev/typetype/server/services/BlockedService.kt +++ b/src/main/kotlin/dev/typetype/server/services/BlockedService.kt @@ -15,7 +15,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll private const val SCOPE_USER = "user" private const val SCOPE_GLOBAL = "global" -class BlockedService(private val eventService: RecommendationEventService? = null) { +class BlockedService { suspend fun getChannels(userId: String): List = DatabaseFactory.query { BlockedChannelsTable.selectAll() .where { (BlockedChannelsTable.userId eq userId) or (BlockedChannelsTable.scope eq SCOPE_GLOBAL) } @@ -55,7 +55,6 @@ class BlockedService(private val eventService: RecommendationEventService? = nul it[blockedAt] = now } } - eventService?.add(userId, "block_channel", null, url, name, null, null) return BlockedItem(url = url, name = name, thumbnailUrl = thumbnailUrl, blockedAt = now) } @@ -69,7 +68,6 @@ class BlockedService(private val eventService: RecommendationEventService? = nul it[blockedAt] = now } } - eventService?.add(userId, "block_video", url, null, null, null, null) return BlockedItem(url = url, blockedAt = now) } diff --git a/src/main/kotlin/dev/typetype/server/services/FavoritesService.kt b/src/main/kotlin/dev/typetype/server/services/FavoritesService.kt index f02a3e5..36658fa 100644 --- a/src/main/kotlin/dev/typetype/server/services/FavoritesService.kt +++ b/src/main/kotlin/dev/typetype/server/services/FavoritesService.kt @@ -11,10 +11,7 @@ import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.selectAll -class FavoritesService( - private val eventService: RecommendationEventService? = null, - private val privacyService: RecommendationPrivacyService = RecommendationPrivacyService(SettingsService()), -) { +class FavoritesService { suspend fun getAll(userId: String): List = DatabaseFactory.query { FavoritesTable.selectAll() @@ -32,17 +29,6 @@ class FavoritesService( it[favoritedAt] = now } } - if (privacyService.isPersonalizationEnabled(userId)) { - eventService?.add( - userId = userId, - eventType = "favorite", - videoUrl = videoUrl, - uploaderUrl = null, - title = null, - watchRatio = null, - watchDurationMs = null, - ) - } return FavoriteItem(videoUrl = videoUrl, favoritedAt = now) } diff --git a/src/main/kotlin/dev/typetype/server/services/HistoryService.kt b/src/main/kotlin/dev/typetype/server/services/HistoryService.kt index 1b9276c..fe98fd1 100644 --- a/src/main/kotlin/dev/typetype/server/services/HistoryService.kt +++ b/src/main/kotlin/dev/typetype/server/services/HistoryService.kt @@ -18,10 +18,7 @@ import org.jetbrains.exposed.v1.jdbc.batchInsert import org.jetbrains.exposed.v1.jdbc.selectAll import java.util.UUID -class HistoryService( - private val eventService: RecommendationEventService? = null, - private val privacyService: RecommendationPrivacyService = RecommendationPrivacyService(SettingsService()), -) { +class HistoryService { suspend fun search(userId: String, q: String?, from: Long?, to: Long?, limit: Int, offset: Int): Pair, Long> = DatabaseFactory.query { val query = HistoryTable.selectAll().where { HistoryTable.userId eq userId } if (!q.isNullOrBlank()) { @@ -90,18 +87,6 @@ class HistoryService( it[HistoryTable.watchedAt] = watchedAt } } - val ratio = if (item.duration > 0) progress.toDouble() / item.duration.toDouble() else 0.0 - if (privacyService.isPersonalizationEnabled(userId)) { - eventService?.add( - userId = userId, - eventType = "watch", - videoUrl = item.url, - uploaderUrl = item.channelUrl, - title = item.title, - watchRatio = ratio.coerceIn(0.0, 1.0), - watchDurationMs = progress * 1_000L, - ) - } return item.copy(id = id, progress = progress, watchedAt = watchedAt) } } diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationBuilder.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationBuilder.kt index 7ddbcd2..3185e8d 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationBuilder.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationBuilder.kt @@ -7,57 +7,29 @@ class HomeRecommendationBuilder( private val subscriptionFeedService: SubscriptionFeedService, private val subscriptionShortsFeedService: SubscriptionShortsFeedService, private val historyService: HistoryService, - private val playlistService: PlaylistService, private val favoritesService: FavoritesService, private val watchLaterService: WatchLaterService, private val blockedService: BlockedService, - private val eventService: RecommendationEventService, - private val feedbackService: RecommendationFeedbackService, - private val feedHistoryService: RecommendationFeedHistoryService, private val signalContextService: HomeRecommendationSignalContextService, - private val trendingService: TrendingService, - private val searchService: SearchService, private val streamService: StreamService, ) { suspend fun build( userId: String, serviceId: Int, mode: HomeRecommendationPoolMode, - personalizationEnabled: Boolean, context: HomeRecommendationContext, ): HomeRecommendationPool { val signalService = HomeRecommendationUserSignalService( subscriptionsService = subscriptionsService, historyService = historyService, - playlistService = playlistService, favoritesService = favoritesService, watchLaterService = watchLaterService, blockedService = blockedService, - recommendationEventService = eventService, - feedHistoryService = feedHistoryService, - feedbackSignalService = RecommendationFeedbackSignalService(feedbackService), - interestProfileService = RecommendationInterestProfileService(), ) - val profile = signalService.loadProfile( - userId = userId, - personalizationEnabled = personalizationEnabled, - sessionContext = context.sessionContext, - ) - val allEvents = if (!personalizationEnabled) emptyList() else eventService.getAll(userId) - val personaState = HomeRecommendationPersonaTracker.infer( - events = allEvents, - sessionContext = context.sessionContext, - ) - val effectiveContext = when (personaState.persona) { - HomeRecommendationSessionPersona.AUTO -> context.sessionContext - HomeRecommendationSessionPersona.QUICK -> context.sessionContext.copy(intent = HomeRecommendationSessionIntent.QUICK) - HomeRecommendationSessionPersona.DEEP -> context.sessionContext.copy(intent = HomeRecommendationSessionIntent.DEEP) - } + val profile = signalService.loadProfile(userId = userId) val candidates = HomeRecommendationCandidateService( subscriptionFeedService = subscriptionFeedService, subscriptionShortsFeedService = subscriptionShortsFeedService, - trendingService = trendingService, - searchService = searchService, streamService = streamService, ) val signalContext = signalContextService.load(userId) @@ -72,41 +44,9 @@ class HomeRecommendationBuilder( profile = profile, subscriptionCandidates = candidatePool.subscriptions, discoveryCandidates = candidatePool.discovery, - context = effectiveContext, + context = context.sessionContext, mode = mode, ) - val sourceWeights = if (!personalizationEnabled) { - emptyMap() - } else { - val classic = HomeRecommendationSourceBandit.weightBySource( - events = allEvents, - sourceByUrl = pool.sourceByUrl, - ) - val contextual = HomeRecommendationContextualBandit.weightBySource( - events = allEvents, - sourceByUrl = pool.sourceByUrl, - serviceId = serviceId, - sessionIntent = effectiveContext.intent, - deviceClass = effectiveContext.deviceClass, - ) - (classic.keys + contextual.keys).associateWith { source -> - val a = classic[source] ?: 1.0 - val b = contextual[source] ?: 1.0 - ((a + b) / 2.0).coerceIn(0.45, 1.55) - } - } - val mergedWeights = (pool.sourceWeights.keys + sourceWeights.keys).associateWith { source -> - val poolWeight = pool.sourceWeights[source] ?: 1.0 - val banditWeight = sourceWeights[source] ?: 1.0 - ((poolWeight + banditWeight) / 2.0).coerceIn(0.55, 1.55) - } - val explorationWeights = if (!personalizationEnabled) mergedWeights else { - HomeRecommendationExplorationBandit.apply( - events = allEvents, - sourceWeights = mergedWeights, - sourceByUrl = pool.sourceByUrl, - ) - } - return pool.copy(sourceWeights = explorationWeights) + return pool } } diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationCandidateService.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationCandidateService.kt index 8b846b7..e826b79 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationCandidateService.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationCandidateService.kt @@ -5,13 +5,10 @@ import dev.typetype.server.models.VideoItem class HomeRecommendationCandidateService( private val subscriptionFeedService: SubscriptionFeedService, private val subscriptionShortsFeedService: SubscriptionShortsFeedService, - private val trendingService: TrendingService, - private val searchService: SearchService, private val streamService: StreamService, private val discoveryAssembler: HomeRecommendationDiscoveryAssembler = HomeRecommendationDiscoveryAssembler(), private val shortsCandidateService: HomeRecommendationShortsCandidateService = HomeRecommendationShortsCandidateService(), ) { - private val searchCandidateFetcher = HomeRecommendationSearchCandidateFetcher(searchService, trendingService) private val relatedCandidateService = HomeRecommendationRelatedCandidateService(streamService) suspend fun fetchCandidates( @@ -70,18 +67,6 @@ class HomeRecommendationCandidateService( return pages.flatten() } - suspend fun fetchTrendingCandidates(serviceId: Int): List = - searchCandidateFetcher.fetchTrendingCandidates(serviceId) - - suspend fun fetchSearchCandidates( - serviceId: Int, - queries: List, - maxQueries: Int, - perQueryLimit: Int, - source: HomeRecommendationSourceTag, - ): List = - searchCandidateFetcher.fetchSearchCandidates(serviceId, queries, maxQueries, perQueryLimit, source) - suspend fun fetchRelatedCandidates( seedUrls: List, source: HomeRecommendationSourceTag, diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationDiscoveryAssembler.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationDiscoveryAssembler.kt index e4c841d..968c85a 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationDiscoveryAssembler.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationDiscoveryAssembler.kt @@ -6,32 +6,12 @@ class HomeRecommendationDiscoveryAssembler { candidates: List, explorationCap: Int, ): List { - val minThemeScore = if (profile.themeTokens.size < 8) 0.24 else 0.34 - val rawDiscovery = candidates + return candidates .asSequence() .filter { tagged -> tagged.video.uploaderUrl !in profile.subscriptionChannels } - .filter { tagged -> tagged.video.url !in profile.feedbackBlockedVideos } - .filter { tagged -> tagged.video.uploaderUrl !in profile.feedbackBlockedChannels } .filter { tagged -> HomeRecommendationLiveTitleDetector.isLiveLike(tagged.video.title).not() } - .filter { tagged -> (profile.channelInterest[tagged.video.uploaderUrl] ?: 0.0) > -1.5 } .distinctBy { tagged -> tagged.video.url } - .toList() - val languagePreferred = rawDiscovery.filter { tagged -> - HomeRecommendationLanguageGate.isLikelyPreferred(tagged.video, profile) - } - val thematic = languagePreferred.filter { tagged -> - HomeRecommendationThemeExtractor.computeThemeScore( - tagged.video.title, - tagged.video.uploaderName, - profile.themeTokens, - ) >= minThemeScore - } - val source = if (languagePreferred.isEmpty()) rawDiscovery else languagePreferred - val thematicUrls = thematic.map { it.video.url }.toSet() - val exploration = source.asSequence() - .filter { tagged -> tagged.video.url !in thematicUrls } .take(explorationCap) .toList() - return (thematic + exploration).distinctBy { tagged -> tagged.video.url } } } diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPageBuilder.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPageBuilder.kt index e2dbb62..1e1a148 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPageBuilder.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPageBuilder.kt @@ -7,15 +7,11 @@ object HomeRecommendationPageBuilder { args: HomeRecommendationApiArgs, mode: HomeRecommendationPoolMode, poolResolver: HomeRecommendationPoolResolver, - feedHistoryService: RecommendationFeedHistoryService, - privacyService: RecommendationPrivacyService, ): HomeRecommendationsResponse { - val personalizationEnabled = privacyService.isPersonalizationEnabled(args.userId) val pool = poolResolver.resolve( userId = args.userId, serviceId = args.serviceId, mode = mode, - personalizationEnabled = personalizationEnabled, context = args.context, ) val page = HomeRecommendationMixer.mix( @@ -57,9 +53,6 @@ object HomeRecommendationPageBuilder { } else { page } - if (personalizationEnabled) { - feedHistoryService.recordShown(args.userId, finalPage.items.map { it.url }) - } return HomeRecommendationsResponse( items = finalPage.items, nextCursor = finalPage.nextCursor, diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolBuilder.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolBuilder.kt index 97813f6..629bd21 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolBuilder.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolBuilder.kt @@ -20,11 +20,6 @@ class HomeRecommendationPoolBuilder { minThemeScore = 0.0, shortsOnly = shortsMode, ) - val jitteredSubscriptions = if (profile.personalizationEnabled) { - HomeRecommendationJitter.apply(subscriptionsScored, profile.feedHistory) - } else { - subscriptionsScored - } val subscriptionUrls = subscriptionsScored.map { it.video.url }.toSet() val discoveryScored = scoreAndFilter( candidates = discoveryCandidates, @@ -34,16 +29,11 @@ class HomeRecommendationPoolBuilder { minThemeScore = 0.0, shortsOnly = shortsMode, ).filterNot { scored -> scored.video.url in subscriptionUrls } - val jitteredDiscovery = if (profile.personalizationEnabled) { - HomeRecommendationJitter.apply(discoveryScored, profile.feedHistory) - } else { - discoveryScored - } return HomeRecommendationPool( - subscriptions = jitteredSubscriptions.map { it.video }, - discovery = jitteredDiscovery.map { it.video }, + subscriptions = subscriptionsScored.map { it.video }, + discovery = discoveryScored.map { it.video }, subscriptionChannels = profile.subscriptionChannels, - sourceByUrl = (jitteredSubscriptions + jitteredDiscovery).associate { it.video.url to it.source }, + sourceByUrl = (subscriptionsScored + discoveryScored).associate { it.video.url to it.source }, sourceWeights = HomeRecommendationPoolWeights.forMode(profile, shortsMode), ) } @@ -66,26 +56,7 @@ class HomeRecommendationPoolBuilder { if (video.uploaderUrl.isNotBlank() && video.uploaderUrl in profile.blockedChannels) return@forEach if (video.uploaderUrl.isNotBlank() && video.uploaderUrl in profile.feedbackBlockedChannels) return@forEach if (!allowLive && HomeRecommendationLiveTitleDetector.isLiveLike(video.title)) return@forEach - if (minThemeScore > 0.0 && profile.themeTokens.isNotEmpty()) { - val themeScore = HomeRecommendationThemeExtractor.computeThemeScore(video.title, video.uploaderName, profile.themeTokens) - if (themeScore < minThemeScore) return@forEach - } - val rawScore = scorer(video, profile) * (profile.eventPenaltyByVideo[video.url] ?: 1.0) - val shortAdjustedScore = if (video.isShortFormContent) { - rawScore * (profile.shortPenaltyByVideo[video.url] ?: 1.0) - } else { - rawScore - } - val seenAdjustedScore = if (video.isShortFormContent) { - shortAdjustedScore * HomeRecommendationShortsSeenMemory.penalty(profile.feedHistory[video.url]) - } else { - shortAdjustedScore - } - val score = if (profile.personalizationEnabled) { - HomeRecommendationScoring.applyPersonalizationPenalties(video, seenAdjustedScore, profile) - } else { - seenAdjustedScore - } + val score = scorer(video, profile) val scored = HomeRecommendationScoredVideo(video = video, score = score, source = tagged.source) val current = byUrl[video.url] if (current == null || scored.score > current.score) byUrl[video.url] = scored diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolver.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolver.kt index 8f4d55a..085e9fd 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolver.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolver.kt @@ -20,18 +20,17 @@ class HomeRecommendationPoolResolver( userId: String, serviceId: Int, mode: HomeRecommendationPoolMode, - personalizationEnabled: Boolean, context: HomeRecommendationContext, ): HomeRecommendationPool { val key = poolCache.key( userId = userId, serviceId = serviceId, mode = mode, - personalizationEnabled = personalizationEnabled, + personalizationEnabled = false, ) val cached = poolCache.read(key) if (cached != null) return cached - val fullBuild = fullBuild(key, userId, serviceId, mode, personalizationEnabled, context) + val fullBuild = fullBuild(key, userId, serviceId, mode, context) val quickFull = withTimeoutOrNull(FULL_BUILD_BUDGET_MS) { fullBuild.await() } if (quickFull != null) { poolCache.write(key, quickFull) @@ -39,7 +38,7 @@ class HomeRecommendationPoolResolver( } schedulePersistence(key, fullBuild) val fastMode = if (mode == HomeRecommendationPoolMode.SHORTS) HomeRecommendationPoolMode.SHORTS else HomeRecommendationPoolMode.FAST - return buildPool(userId, serviceId, fastMode, personalizationEnabled, context) + return buildPool(userId, serviceId, fastMode, context) } private fun fullBuild( @@ -47,13 +46,12 @@ class HomeRecommendationPoolResolver( userId: String, serviceId: Int, mode: HomeRecommendationPoolMode, - personalizationEnabled: Boolean, context: HomeRecommendationContext, ): Deferred { state.fullBuilds[key]?.let { return it } val created = scope.async { val fullMode = if (mode == HomeRecommendationPoolMode.SHORTS) HomeRecommendationPoolMode.SHORTS else HomeRecommendationPoolMode.FULL - buildPool(userId, serviceId, fullMode, personalizationEnabled, context) + buildPool(userId, serviceId, fullMode, context) } val winner = state.fullBuilds.putIfAbsent(key, created) if (winner != null) { @@ -68,29 +66,21 @@ class HomeRecommendationPoolResolver( userId: String, serviceId: Int, mode: HomeRecommendationPoolMode, - personalizationEnabled: Boolean, context: HomeRecommendationContext, ): HomeRecommendationPool = HomeRecommendationBuilder( subscriptionsService = dependencies.subscriptionsService, subscriptionFeedService = dependencies.subscriptionFeedService, subscriptionShortsFeedService = dependencies.subscriptionShortsFeedService, historyService = dependencies.historyService, - playlistService = dependencies.playlistService, favoritesService = dependencies.favoritesService, watchLaterService = dependencies.watchLaterService, blockedService = dependencies.blockedService, - eventService = dependencies.eventService, - feedbackService = dependencies.feedbackService, - feedHistoryService = dependencies.feedHistoryService, - trendingService = dependencies.trendingService, - searchService = dependencies.searchService, streamService = dependencies.streamService, signalContextService = dependencies.signalContextService, ).build( userId = userId, serviceId = serviceId, mode = mode, - personalizationEnabled = personalizationEnabled, context = context, ) diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolverDependencies.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolverDependencies.kt index e86e325..1b8649a 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolverDependencies.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationPoolResolverDependencies.kt @@ -7,16 +7,10 @@ data class HomeRecommendationPoolResolverDependencies( val subscriptionFeedService: SubscriptionFeedService, val subscriptionShortsFeedService: SubscriptionShortsFeedService, val historyService: HistoryService, - val playlistService: PlaylistService = PlaylistService(), val favoritesService: FavoritesService, val watchLaterService: WatchLaterService, val blockedService: BlockedService, - val feedbackService: RecommendationFeedbackService, - val eventService: RecommendationEventService, - val feedHistoryService: RecommendationFeedHistoryService, val signalContextService: HomeRecommendationSignalContextService, - val trendingService: TrendingService, - val searchService: SearchService, val streamService: StreamService = HomeRecommendationNoopStreamService, val cache: CacheService, ) diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationScoring.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationScoring.kt index 9608adf..6919da4 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationScoring.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationScoring.kt @@ -9,7 +9,7 @@ object HomeRecommendationScoring { context: HomeRecommendationSessionContext, ): Double { val base = scoreSubscription(video, profile) - return base + HomeRecommendationSessionIntentScorer.bonus(video, context) + HomeRecommendationDeviceScorer.bonus(video, context) + return base + contextBoost(video, context) } fun scoreDiscovery( @@ -18,55 +18,27 @@ object HomeRecommendationScoring { context: HomeRecommendationSessionContext, ): Double { val base = scoreDiscovery(video, profile) - return base + HomeRecommendationSessionIntentScorer.bonus(video, context) + HomeRecommendationDeviceScorer.bonus(video, context) + return base + contextBoost(video, context) } - fun scoreShortsDiscovery( - video: VideoItem, - profile: HomeRecommendationProfile, - context: HomeRecommendationSessionContext, - ): Double = scoreDiscovery(video, profile, context) + HomeRecommendationShortsProfileFit.score(video, profile) + fun scoreShortsDiscovery(video: VideoItem, profile: HomeRecommendationProfile, context: HomeRecommendationSessionContext): Double = + scoreDiscovery(video, profile, context) fun scoreSubscription(video: VideoItem, profile: HomeRecommendationProfile): Double { - val base = 1.25 - return base + commonSignals(video, profile) + 0.25 + return 1.5 + commonSignals(video, profile) } fun scoreDiscovery(video: VideoItem, profile: HomeRecommendationProfile): Double { - val base = 0.95 - val themeScore = HomeRecommendationThemeExtractor.computeThemeScore( - videoTitle = video.title, - channelName = video.uploaderName, - themeTokens = profile.themeTokens, - ) - val themeBoost = themeScore * 0.8 - val channelBoost = (profile.channelInterest[video.uploaderUrl] ?: 0.0).coerceIn(-5.0, 8.0) * 0.08 - val topicBoost = topicBoost(video, profile.topicInterest) - val delayedVideoBoost = (profile.delayedVideoCredit[video.url] ?: 0.0).coerceIn(0.0, 2.0) * 0.18 - val delayedChannelBoost = (profile.delayedChannelCredit[video.uploaderUrl] ?: 0.0).coerceIn(0.0, 2.0) * 0.12 - return base + commonSignals(video, profile) + themeBoost + channelBoost + topicBoost + delayedVideoBoost + delayedChannelBoost + return 1.0 + commonSignals(video, profile) } private fun commonSignals(video: VideoItem, profile: HomeRecommendationProfile): Double { val recency = recencyBoost(video.uploaded) - val keywordBoost = keywordBoost(video, profile.keywordAffinity) - val temporalBoost = HomeRecommendationTemporalBoost.boost(video.title) - val viralBoost = HomeRecommendationPersonalization.viralVelocityBoost(video) - val curiosityBoost = HomeRecommendationPersonalization.curiosityBoost(video, profile) - val serendipityBoost = HomeRecommendationPersonalization.serendipityBoost(video, profile) - val channelProfileBoost = HomeRecommendationPersonalization.channelProfileBoost(video, profile) val subscriptionBoost = if (video.uploaderUrl in profile.subscriptionChannels) 0.2 else 0.0 val favoriteBoost = if (video.url in profile.favoriteUrls) 0.15 else 0.0 val watchLaterBoost = if (video.url in profile.watchLaterUrls) 0.08 else 0.0 val livePenalty = if (HomeRecommendationLiveTitleDetector.isLiveLike(video.title)) -0.5 else 0.0 - return recency + keywordBoost + temporalBoost + viralBoost + curiosityBoost + serendipityBoost + channelProfileBoost + subscriptionBoost + favoriteBoost + watchLaterBoost + livePenalty - } - - private fun keywordBoost(video: VideoItem, keywords: Set): Double { - if (keywords.isEmpty()) return 0.0 - val titleTokens = video.title.lowercase().split(Regex("[^a-z0-9]+")) - val hits = titleTokens.count { token -> token in keywords } - return (hits.coerceAtMost(4)) * 0.06 + return recency + subscriptionBoost + favoriteBoost + watchLaterBoost + livePenalty } private fun recencyBoost(uploaded: Long): Double { @@ -75,16 +47,11 @@ object HomeRecommendationScoring { return 1.0 / (1.0 + ageHours / 60.0) } - private fun topicBoost(video: VideoItem, topicInterest: Map): Double { - if (topicInterest.isEmpty()) return 0.0 - val tokens = RecommendationTopicTokenizer.tokenize("${video.title} ${video.uploaderName}") - if (tokens.isEmpty()) return 0.0 - val raw = tokens.sumOf { token -> topicInterest[token] ?: 0.0 } - return raw.coerceIn(-4.0, 10.0) * 0.06 - } - - fun applyPersonalizationPenalties(video: VideoItem, score: Double, profile: HomeRecommendationProfile): Double { - return HomeRecommendationPersonalization.applyPenalties(video, score, profile) - } + private fun contextBoost(video: VideoItem, context: HomeRecommendationSessionContext): Double = + when (context.intent) { + HomeRecommendationSessionIntent.QUICK -> if (video.duration in 1L..600L) 0.05 else 0.0 + HomeRecommendationSessionIntent.DEEP -> if (video.duration >= 1_200L) 0.05 else 0.0 + HomeRecommendationSessionIntent.AUTO -> 0.0 + } } diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationService.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationService.kt index 5f9438e..5943417 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationService.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationService.kt @@ -4,8 +4,6 @@ import dev.typetype.server.models.HomeRecommendationsResponse class HomeRecommendationService( private val poolResolver: HomeRecommendationPoolResolver, - private val feedHistoryService: RecommendationFeedHistoryService, - private val privacyService: RecommendationPrivacyService, ) { private fun args( userId: String, @@ -34,8 +32,6 @@ class HomeRecommendationService( args = args(userId, serviceId, limit, cursor, context, debug), mode = HomeRecommendationPoolMode.FULL, poolResolver = poolResolver, - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) suspend fun getShorts( @@ -49,7 +45,5 @@ class HomeRecommendationService( args = args(userId, serviceId, limit, cursor, context, debug), mode = HomeRecommendationPoolMode.SHORTS, poolResolver = poolResolver, - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) } diff --git a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationUserSignalService.kt b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationUserSignalService.kt index c659b4d..e34126f 100644 --- a/src/main/kotlin/dev/typetype/server/services/HomeRecommendationUserSignalService.kt +++ b/src/main/kotlin/dev/typetype/server/services/HomeRecommendationUserSignalService.kt @@ -3,113 +3,34 @@ package dev.typetype.server.services class HomeRecommendationUserSignalService( private val subscriptionsService: SubscriptionsService, private val historyService: HistoryService, - private val playlistService: PlaylistService, private val favoritesService: FavoritesService, private val watchLaterService: WatchLaterService, private val blockedService: BlockedService, - private val recommendationEventService: RecommendationEventService, - private val feedHistoryService: RecommendationFeedHistoryService, - private val feedbackSignalService: RecommendationFeedbackSignalService, - private val interestProfileService: RecommendationInterestProfileService, ) { - suspend fun loadProfile(userId: String, personalizationEnabled: Boolean): HomeRecommendationProfile { - return loadProfile( - userId = userId, - personalizationEnabled = personalizationEnabled, - sessionContext = HomeRecommendationSessionContext( - intent = HomeRecommendationSessionIntent.AUTO, - deviceClass = HomeRecommendationDeviceClass.UNKNOWN, - ), - ) - } - - suspend fun loadProfile( - userId: String, - personalizationEnabled: Boolean, - sessionContext: HomeRecommendationSessionContext, - ): HomeRecommendationProfile { + suspend fun loadProfile(userId: String): HomeRecommendationProfile { val subscriptions = subscriptionsService.getAll(userId) - val favorites = if (personalizationEnabled) favoritesService.getAll(userId) else emptyList() - val watchLater = if (personalizationEnabled) watchLaterService.getAll(userId) else emptyList() - val playlists = if (personalizationEnabled) playlistService.getAll(userId) else emptyList() - val historyItems = if (personalizationEnabled) { - historyService.search(userId = userId, q = null, from = null, to = null, limit = 240, offset = 0).first - } else { - emptyList() - } + val favorites = favoritesService.getAll(userId) + val watchLater = watchLaterService.getAll(userId) + val historyItems = historyService.search(userId = userId, q = null, from = null, to = null, limit = 240, offset = 0).first val blockedVideos = blockedService.getVideos(userId).map { it.url }.toSet() val blockedChannels = blockedService.getChannels(userId).map { it.url }.toSet() - val feedbackSignals = feedbackSignalService.load(userId) - val events = if (personalizationEnabled) recommendationEventService.getAll(userId) else emptyList() - val eventSignals = HomeRecommendationEventAnalyzer.buildSignals(events) - val interestProfile = if (personalizationEnabled) interestProfileService.load(userId) else RecommendationInterestProfile(emptyMap(), emptyMap()) - val feedHistory = if (personalizationEnabled) feedHistoryService.load(userId) else emptyMap() - val delayedVideoCredit = if (personalizationEnabled) { - HomeRecommendationDelayedCreditBuilder.buildVideoCredit(historyItems, favorites, watchLater, playlists) - } else { - emptyMap() - } - val delayedChannelCredit = if (personalizationEnabled) { - HomeRecommendationDelayedCreditBuilder.buildChannelCredit(historyItems) - } else { - emptyMap() - } - val seenUrls = if (personalizationEnabled) historyItems.map { it.url }.toSet() else emptySet() + val seenUrls = historyItems.map { it.url }.toSet() val favoriteUrls = favorites.map { it.videoUrl }.toSet() val watchLaterUrls = watchLater.map { it.url }.toSet() - val keywordAffinity = if (personalizationEnabled) historyItems - .asSequence() - .map { it.title.lowercase() } - .flatMap { title -> title.split(Regex("[^a-z0-9]+")) } - .filter { token -> token.length >= 4 } - .groupingBy { it } - .eachCount() - .entries - .sortedByDescending { it.value } - .take(30) - .map { it.key } - .toSet() else emptySet() - val themeTokens = if (personalizationEnabled) { - HomeRecommendationThemeExtractor.extractThemeTokens(subscriptions = subscriptions, watchLater = watchLater) - } else { - emptySet() - } val subscriptionChannels = subscriptions.map { it.channelUrl }.toSet() - val engagementSplit = if (personalizationEnabled) { - HomeRecommendationEngagementSplitCalculator.compute(events, subscriptionChannels) - } else { - HomeRecommendationEngagementSplit(0.0, 0.0) - } - val themeQueries = if (personalizationEnabled) HomeRecommendationThemeExtractor.buildThemeQueries(themeTokens) else emptyList() return HomeRecommendationProfile( seenUrls = seenUrls, blockedVideos = blockedVideos, blockedChannels = blockedChannels, - feedbackBlockedVideos = feedbackSignals.blockedVideos, - feedbackBlockedChannels = feedbackSignals.blockedUploaders, + feedbackBlockedVideos = emptySet(), + feedbackBlockedChannels = emptySet(), subscriptionChannels = subscriptionChannels, favoriteUrls = favoriteUrls, watchLaterUrls = watchLaterUrls, - keywordAffinity = keywordAffinity, - themeTokens = themeTokens, - themeQueries = themeQueries, - channelInterest = interestProfile.channelScores, - topicInterest = interestProfile.topicScores, - eventPenaltyByVideo = eventSignals.videoPenalty, - implicitBlockedVideos = eventSignals.implicitBlockedVideos, - subscriptionEngagement = engagementSplit.subscriptionEngagement, - discoveryEngagement = engagementSplit.discoveryEngagement, - feedHistory = feedHistory, - rejectionTopicPenalty = eventSignals.rejectionTopicPenalty, - rejectionChannelPenalty = eventSignals.rejectionChannelPenalty, - channelTopicProfile = HomeRecommendationSignalProfileBuilder.buildChannelTopicProfile(historyItems), - shortsTopicInterest = HomeRecommendationSignalProfileBuilder.buildShortsTopicInterest(events), - rejectionTopicPairPenalty = HomeRecommendationSignalProfileBuilder.buildTopicPairPenalty(events), - creatorMomentum = HomeRecommendationSignalProfileBuilder.buildCreatorMomentum(events), - delayedVideoCredit = delayedVideoCredit, - delayedChannelCredit = delayedChannelCredit, - shortPenaltyByVideo = HomeRecommendationShortsSignals.shortSkipPenaltyByUrl(events), - personalizationEnabled = personalizationEnabled, + keywordAffinity = emptySet(), + themeTokens = emptySet(), + themeQueries = emptyList(), + personalizationEnabled = false, ) } } diff --git a/src/main/kotlin/dev/typetype/server/services/RecommendationEventService.kt b/src/main/kotlin/dev/typetype/server/services/RecommendationEventService.kt index 778f8f8..b4534ca 100644 --- a/src/main/kotlin/dev/typetype/server/services/RecommendationEventService.kt +++ b/src/main/kotlin/dev/typetype/server/services/RecommendationEventService.kt @@ -13,10 +13,8 @@ import java.util.UUID class RecommendationEventService( private val interestService: RecommendationInterestService, - private val privacyService: RecommendationPrivacyService = RecommendationPrivacyService(SettingsService()), ) { suspend fun hasClick(userId: String): Boolean { - if (!privacyService.isPersonalizationEnabled(userId)) return false return DatabaseFactory.query { RecommendationEventsTable.selectAll() .where { (RecommendationEventsTable.userId eq userId) and (RecommendationEventsTable.eventType eq "click") } @@ -26,7 +24,6 @@ class RecommendationEventService( } suspend fun getAll(userId: String): List { - if (!privacyService.isPersonalizationEnabled(userId)) return emptyList() return DatabaseFactory.query { RecommendationEventsTable.selectAll() .where { RecommendationEventsTable.userId eq userId } @@ -46,21 +43,6 @@ class RecommendationEventService( watchDurationMs: Long?, contextKey: String? = null, ): RecommendationEventItem { - if (!privacyService.isPersonalizationEnabled(userId)) { - val now = System.currentTimeMillis() - return RecommendationEventItem( - id = "disabled", - eventType = eventType, - videoUrl = videoUrl, - uploaderUrl = uploaderUrl, - title = title, - watchRatio = watchRatio, - watchDurationMs = watchDurationMs, - contextKey = contextKey, - occurredAt = now, - publishedAt = now, - ) - } val id = UUID.randomUUID().toString() val now = System.currentTimeMillis() DatabaseFactory.query { diff --git a/src/main/kotlin/dev/typetype/server/services/RecommendationFeedbackService.kt b/src/main/kotlin/dev/typetype/server/services/RecommendationFeedbackService.kt index b6a0290..77eaedc 100644 --- a/src/main/kotlin/dev/typetype/server/services/RecommendationFeedbackService.kt +++ b/src/main/kotlin/dev/typetype/server/services/RecommendationFeedbackService.kt @@ -12,10 +12,8 @@ import java.util.UUID class RecommendationFeedbackService( private val eventService: RecommendationEventService, - private val privacyService: RecommendationPrivacyService = RecommendationPrivacyService(SettingsService()), ) { suspend fun getAll(userId: String): List { - if (!privacyService.isPersonalizationEnabled(userId)) return emptyList() return DatabaseFactory.query { RecommendationFeedbackTable.selectAll() .where { RecommendationFeedbackTable.userId eq userId } @@ -26,15 +24,6 @@ class RecommendationFeedbackService( } suspend fun add(userId: String, feedbackType: String, videoUrl: String?, uploaderUrl: String?): RecommendationFeedbackItem { - if (!privacyService.isPersonalizationEnabled(userId)) { - return RecommendationFeedbackItem( - id = "disabled", - feedbackType = feedbackType, - videoUrl = videoUrl, - uploaderUrl = uploaderUrl, - createdAt = System.currentTimeMillis(), - ) - } val id = UUID.randomUUID().toString() val now = System.currentTimeMillis() DatabaseFactory.query { diff --git a/src/main/kotlin/dev/typetype/server/services/RecommendationPrivacyService.kt b/src/main/kotlin/dev/typetype/server/services/RecommendationPrivacyService.kt index 9e1cc58..6a620c8 100644 --- a/src/main/kotlin/dev/typetype/server/services/RecommendationPrivacyService.kt +++ b/src/main/kotlin/dev/typetype/server/services/RecommendationPrivacyService.kt @@ -1,6 +1,5 @@ package dev.typetype.server.services -class RecommendationPrivacyService(private val settingsService: SettingsService) { - suspend fun isPersonalizationEnabled(userId: String): Boolean = - settingsService.get(userId).recommendationPersonalizationEnabled +class RecommendationPrivacyService { + fun isPersonalizationEnabled(): Boolean = false } diff --git a/src/main/kotlin/dev/typetype/server/services/SettingsService.kt b/src/main/kotlin/dev/typetype/server/services/SettingsService.kt index 3518183..844b18c 100644 --- a/src/main/kotlin/dev/typetype/server/services/SettingsService.kt +++ b/src/main/kotlin/dev/typetype/server/services/SettingsService.kt @@ -22,7 +22,6 @@ class SettingsService { defaultSubtitleLanguage = it[SettingsTable.defaultSubtitleLanguage], defaultAudioLanguage = it[SettingsTable.defaultAudioLanguage], preferOriginalLanguage = it[SettingsTable.preferOriginalLanguage], - recommendationPersonalizationEnabled = it[SettingsTable.recommendationPersonalizationEnabled], ) } ?: SettingsItem() } @@ -39,7 +38,6 @@ class SettingsService { it[defaultSubtitleLanguage] = settings.defaultSubtitleLanguage it[defaultAudioLanguage] = settings.defaultAudioLanguage it[preferOriginalLanguage] = settings.preferOriginalLanguage - it[recommendationPersonalizationEnabled] = settings.recommendationPersonalizationEnabled } if (updated == 0) { SettingsTable.insert { @@ -53,7 +51,6 @@ class SettingsService { it[defaultSubtitleLanguage] = settings.defaultSubtitleLanguage it[defaultAudioLanguage] = settings.defaultAudioLanguage it[preferOriginalLanguage] = settings.preferOriginalLanguage - it[recommendationPersonalizationEnabled] = settings.recommendationPersonalizationEnabled } } } diff --git a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsBlendService.kt b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsBlendService.kt index b0f2e83..a76d71b 100644 --- a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsBlendService.kt +++ b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsBlendService.kt @@ -6,10 +6,8 @@ import dev.typetype.server.models.VideoItem class SubscriptionShortsBlendService( private val trendingService: TrendingService, - private val signalService: SubscriptionShortsSignalService, ) { suspend fun build( - userId: String, subs: List, serviceId: Int, page: Int, @@ -19,9 +17,8 @@ class SubscriptionShortsBlendService( .map { it.toShortCanonicalUrl() } .filter { video -> subs.none { it.toShortDedupKey() == video.toShortDedupKey() } } .distinctBy { it.url } - val signals = signalService.load(userId) - val rankedSubs = subs.sortedByDescending { scoreShort(it, true, signals) } - val rankedDiscovery = discovery.sortedByDescending { scoreShort(it, false, signals) } + val rankedSubs = subs.sortedByDescending { scoreShort(it, true) } + val rankedDiscovery = discovery.sortedByDescending { scoreShort(it, false) } val discoveryPage = rankedDiscovery.drop(page * limit).take(limit) val videos = blend(rankedSubs, discoveryPage, limit) val hasNext = hasMoreSubs(subs, limit) || hasMoreDiscovery(rankedDiscovery, page, limit) @@ -62,10 +59,9 @@ class SubscriptionShortsBlendService( return nextStart < discovery.size } - private fun scoreShort(video: VideoItem, isSubscription: Boolean, penaltyByVideo: Map): Double { + private fun scoreShort(video: VideoItem, isSubscription: Boolean): Double { val ageBoost = if (video.uploaded <= 0L) 0.0 else 1.0 / (1.0 + ((System.currentTimeMillis() - video.uploaded).coerceAtLeast(0L) / 3_600_000.0) / 24.0) val sourceBoost = if (isSubscription) 1.0 else 0.85 - val penalty = penaltyByVideo[video.url] ?: 1.0 - return (sourceBoost + ageBoost) * penalty + return sourceBoost + ageBoost } } diff --git a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsFeedService.kt b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsFeedService.kt index d86b472..089af05 100644 --- a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsFeedService.kt +++ b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsFeedService.kt @@ -31,7 +31,7 @@ class SubscriptionShortsFeedService( suspend fun getBlendedFeed(userId: String, serviceId: Int, page: Int, limit: Int): SubscriptionFeedResponse { val sourcePage = page val subs = getFeed(userId, sourcePage, limit).videos - return blendService.build(userId = userId, subs = subs, serviceId = serviceId, page = page, limit = limit) + return blendService.build(subs = subs, serviceId = serviceId, page = page, limit = limit) } private suspend fun cachedAll(userId: String): List { diff --git a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsSignalService.kt b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsSignalService.kt index 4727b9f..b481d09 100644 --- a/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsSignalService.kt +++ b/src/main/kotlin/dev/typetype/server/services/SubscriptionShortsSignalService.kt @@ -1,23 +1,5 @@ package dev.typetype.server.services -class SubscriptionShortsSignalService(private val recommendationEventService: RecommendationEventService) { - suspend fun load(userId: String): Map { - val events = recommendationEventService.getAll(userId) - val byVideo = events - .asSequence() - .mapNotNull { event -> event.videoUrl?.takeIf { it.isNotBlank() }?.let { it to event } } - .groupBy({ it.first }, { it.second }) - return byVideo.mapValues { (_, items) -> - val skipLight = items.count { it.eventType == "short_skip" && (it.watchDurationMs ?: 0L) in 800L..4_999L } - val skipInstant = items.count { it.eventType == "short_skip" && (it.watchDurationMs ?: 0L) in 0L..799L } - val skipLate = items.count { it.eventType == "short_skip" && (it.watchDurationMs ?: 0L) >= 5_000L } - val watch = items.count { it.eventType == "watch" || it.eventType == "click" } - val skipScore = skipInstant * 1.2 + skipLight * 0.9 + skipLate * 0.4 - when { - skipScore >= 3.0 && watch == 0 -> 0.35 - skipScore >= 1.0 && watch == 0 -> 0.65 - else -> 1.0 - } - } - } +class SubscriptionShortsSignalService { + fun load(): Map = emptyMap() } diff --git a/src/main/kotlin/dev/typetype/server/services/WatchLaterService.kt b/src/main/kotlin/dev/typetype/server/services/WatchLaterService.kt index 3cc168f..ebac7f1 100644 --- a/src/main/kotlin/dev/typetype/server/services/WatchLaterService.kt +++ b/src/main/kotlin/dev/typetype/server/services/WatchLaterService.kt @@ -11,10 +11,7 @@ import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.selectAll -class WatchLaterService( - private val eventService: RecommendationEventService? = null, - private val privacyService: RecommendationPrivacyService = RecommendationPrivacyService(SettingsService()), -) { +class WatchLaterService { suspend fun getAll(userId: String): List = DatabaseFactory.query { WatchLaterTable.selectAll() @@ -35,17 +32,6 @@ class WatchLaterService( it[addedAt] = now } } - if (privacyService.isPersonalizationEnabled(userId)) { - eventService?.add( - userId = userId, - eventType = "watch_later_add", - videoUrl = item.url, - uploaderUrl = null, - title = item.title, - watchRatio = null, - watchDurationMs = null, - ) - } return item.copy(addedAt = now) } diff --git a/src/main/kotlin/dev/typetype/server/services/YoutubeTakeoutPreferenceService.kt b/src/main/kotlin/dev/typetype/server/services/YoutubeTakeoutPreferenceService.kt index ca92686..5a41ef8 100644 --- a/src/main/kotlin/dev/typetype/server/services/YoutubeTakeoutPreferenceService.kt +++ b/src/main/kotlin/dev/typetype/server/services/YoutubeTakeoutPreferenceService.kt @@ -1,12 +1,5 @@ package dev.typetype.server.services -class YoutubeTakeoutPreferenceService(private val recommendationEventService: RecommendationEventService) { - suspend fun preferredCategories(userId: String): List { - val hasClickSignals = recommendationEventService.hasClick(userId) - return if (hasClickSignals) { - listOf("technology", "gaming", "programming", "news") - } else { - listOf("technology", "gaming") - } - } +class YoutubeTakeoutPreferenceService { + fun preferredCategories(): List = listOf("technology", "gaming") } From 15a8c4e83635da0a71f51be15f2a15bb42a3263c Mon Sep 17 00:00:00 2001 From: Priveetee Date: Wed, 6 May 2026 18:03:26 +0200 Subject: [PATCH 2/2] test: remove personalization coverage --- .../HomeRecommendationCandidateServiceTest.kt | 6 +- ...mmendationEngagementSplitCalculatorTest.kt | 41 ------ ...ecommendationEventAnalyzerRejectionTest.kt | 29 ---- .../HomeRecommendationEventAnalyzerTest.kt | 49 ------- ...omeRecommendationFeedHistoryServiceTest.kt | 40 ------ .../server/HomeRecommendationJitterTest.kt | 48 ------- .../HomeRecommendationMetricsRoutesTest.kt | 124 ---------------- .../HomeRecommendationOfflineEvaluatorTest.kt | 52 ------- .../HomeRecommendationPersonalizationTest.kt | 53 ------- ...dationPoolBuilderPersonalizationOffTest.kt | 67 --------- .../HomeRecommendationPoolBuilderTest.kt | 32 ----- ...eRecommendationPoolResolverCacheKeyTest.kt | 73 ---------- .../HomeRecommendationRoutesKillSwitchTest.kt | 90 ------------ .../server/HomeRecommendationRoutesTest.kt | 20 --- .../HomeRecommendationRoutesValidationTest.kt | 20 --- .../HomeRecommendationServiceFastPathTest.kt | 20 --- ...HomeRecommendationShortsDebugRoutesTest.kt | 16 --- .../HomeRecommendationShortsProfileFitTest.kt | 49 ------- ...omeRecommendationShortsQueryFactoryTest.kt | 28 ---- .../HomeRecommendationShortsRoutesTest.kt | 16 --- .../HomeRecommendationShortsSeenMemoryTest.kt | 22 --- .../HomeRecommendationShortsSignalsTest.kt | 35 ----- .../server/HomeRecommendationTestFixtures.kt | 18 +-- ...ecommendationEventItemSerializationTest.kt | 28 ---- .../server/RecommendationEventPrivacyTest.kt | 41 ------ .../server/RecommendationEventsRoutesTest.kt | 134 ------------------ .../RecommendationFeedbackPrivacyTest.kt | 36 ----- .../RecommendationFeedbackRoutesTest.kt | 75 ---------- .../RecommendationInterestServiceTest.kt | 73 ---------- ...commendationInterestWeightShortSkipTest.kt | 12 -- .../RecommendationOnboardingRoutesTest.kt | 90 ------------ ...endationOnboardingSkipReapplyRoutesTest.kt | 97 ------------- .../dev/typetype/server/SettingsRoutesTest.kt | 6 +- .../server/SubscriptionShortsFeedBlendTest.kt | 23 +-- .../SubscriptionShortsFeedDiversityTest.kt | 6 +- .../SubscriptionShortsFeedRoutesTest.kt | 6 +- .../SubscriptionShortsSignalServiceTest.kt | 32 ----- .../dev/typetype/server/TestDatabase.kt | 14 -- 38 files changed, 8 insertions(+), 1613 deletions(-) delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationEngagementSplitCalculatorTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerRejectionTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationFeedHistoryServiceTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationJitterTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationMetricsRoutesTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationOfflineEvaluatorTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationPersonalizationTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderPersonalizationOffTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationPoolResolverCacheKeyTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesKillSwitchTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationShortsProfileFitTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationShortsQueryFactoryTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSeenMemoryTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSignalsTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationEventItemSerializationTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationEventPrivacyTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationEventsRoutesTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationFeedbackPrivacyTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationFeedbackRoutesTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationInterestServiceTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationInterestWeightShortSkipTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationOnboardingRoutesTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/RecommendationOnboardingSkipReapplyRoutesTest.kt delete mode 100644 src/test/kotlin/dev/typetype/server/SubscriptionShortsSignalServiceTest.kt diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationCandidateServiceTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationCandidateServiceTest.kt index c0fb2fb..8b69c4c 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationCandidateServiceTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationCandidateServiceTest.kt @@ -9,11 +9,9 @@ import dev.typetype.server.services.HomeRecommendationPoolMode import dev.typetype.server.services.HomeRecommendationProfile import dev.typetype.server.services.HomeRecommendationSignalContext import dev.typetype.server.services.HomeRecommendationSourceTag -import dev.typetype.server.services.SearchService import dev.typetype.server.services.StreamService import dev.typetype.server.services.SubscriptionFeedService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.TrendingService import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -24,11 +22,9 @@ import org.junit.jupiter.api.Test class HomeRecommendationCandidateServiceTest { private val subscriptionFeedService: SubscriptionFeedService = mockk() private val subscriptionShortsFeedService: SubscriptionShortsFeedService = mockk() - private val trendingService: TrendingService = mockk() - private val searchService: SearchService = mockk() private val streamService: StreamService = mockk() private val service = HomeRecommendationCandidateService( - subscriptionFeedService, subscriptionShortsFeedService, trendingService, searchService, streamService, + subscriptionFeedService, subscriptionShortsFeedService, streamService, ) @BeforeEach diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationEngagementSplitCalculatorTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationEngagementSplitCalculatorTest.kt deleted file mode 100644 index 04ead98..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationEngagementSplitCalculatorTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.RecommendationEventItem -import dev.typetype.server.services.HomeRecommendationEngagementSplitCalculator -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationEngagementSplitCalculatorTest { - @Test - fun `calculator separates subscription and discovery engagement`() { - val events = listOf( - RecommendationEventItem( - id = "1", - eventType = "watch", - videoUrl = "u1", - uploaderUrl = "https://yt.com/c/sub", - title = null, - watchRatio = 0.8, - watchDurationMs = null, - contextKey = null, - occurredAt = 1, - ), - RecommendationEventItem( - id = "2", - eventType = "short_skip", - videoUrl = "u2", - uploaderUrl = "https://yt.com/c/disc", - title = null, - watchRatio = null, - watchDurationMs = 200, - contextKey = null, - occurredAt = 2, - ), - ) - val split = HomeRecommendationEngagementSplitCalculator.compute( - events = events, - subscriptionChannels = setOf("https://yt.com/c/sub"), - ) - assertTrue(split.subscriptionEngagement > split.discoveryEngagement) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerRejectionTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerRejectionTest.kt deleted file mode 100644 index e8e4174..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerRejectionTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.RecommendationEventItem -import dev.typetype.server.services.HomeRecommendationEventAnalyzer -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationEventAnalyzerRejectionTest { - @Test - fun `analyzer emits topic and channel rejection penalties from repeated short skips`() { - val now = System.currentTimeMillis() - val events = (1..6).map { index -> - RecommendationEventItem( - id = index.toString(), - eventType = "short_skip", - videoUrl = "https://yt.com/v/$index", - uploaderUrl = "https://yt.com/c/reject", - title = "music remix $index", - watchRatio = null, - watchDurationMs = 200L, - contextKey = null, - occurredAt = now - (index * 1_000L), - ) - } - val signals = HomeRecommendationEventAnalyzer.buildSignals(events) - assertTrue((signals.rejectionChannelPenalty["https://yt.com/c/reject"] ?: 1.0) < 1.0) - assertTrue((signals.rejectionTopicPenalty["music"] ?: 1.0) < 1.0) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerTest.kt deleted file mode 100644 index b0be2f8..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationEventAnalyzerTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.RecommendationEventItem -import dev.typetype.server.services.HomeRecommendationEventAnalyzer -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationEventAnalyzerTest { - @Test - fun `marks repeated unclicked impressions as implicitly blocked`() { - val now = System.currentTimeMillis() - val events = (1..5).map { index -> - RecommendationEventItem( - id = index.toString(), - eventType = "impression", - videoUrl = "https://yt.com/v/a", - uploaderUrl = "https://yt.com/c/a", - title = null, - watchRatio = null, - contextKey = null, - occurredAt = now - index * 1_000L, - ) - } - val signals = HomeRecommendationEventAnalyzer.buildSignals(events) - assertTrue("https://yt.com/v/a" in signals.implicitBlockedVideos) - assertEquals(0.10, signals.videoPenalty["https://yt.com/v/a"]) - } - - @Test - fun `keeps medium penalty when impressions are repeated without clicks`() { - val now = System.currentTimeMillis() - val events = (1..3).map { index -> - RecommendationEventItem( - id = index.toString(), - eventType = "impression", - videoUrl = "https://yt.com/v/b", - uploaderUrl = "https://yt.com/c/b", - title = null, - watchRatio = null, - contextKey = null, - occurredAt = now - index * 1_000L, - ) - } - val signals = HomeRecommendationEventAnalyzer.buildSignals(events) - assertTrue("https://yt.com/v/b" !in signals.implicitBlockedVideos) - assertEquals(0.30, signals.videoPenalty["https://yt.com/v/b"]) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationFeedHistoryServiceTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationFeedHistoryServiceTest.kt deleted file mode 100644 index 02a19df..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationFeedHistoryServiceTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.services.RecommendationFeedHistoryService -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class HomeRecommendationFeedHistoryServiceTest { - private val service = RecommendationFeedHistoryService() - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `recordShown increments show count`() = runTest { - service.recordShown(TEST_USER_ID, listOf("https://yt.com/v/a")) - service.recordShown(TEST_USER_ID, listOf("https://yt.com/v/a")) - val loaded = service.load(TEST_USER_ID) - assertEquals(2, loaded["https://yt.com/v/a"]?.showCount) - } - - @Test - fun `recordShown stores unique urls only once per call`() = runTest { - service.recordShown(TEST_USER_ID, listOf("https://yt.com/v/a", "https://yt.com/v/a")) - val loaded = service.load(TEST_USER_ID) - assertTrue("https://yt.com/v/a" in loaded.keys) - assertEquals(1, loaded["https://yt.com/v/a"]?.showCount) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationJitterTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationJitterTest.kt deleted file mode 100644 index d5ad73a..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationJitterTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.HomeRecommendationJitter -import dev.typetype.server.services.HomeRecommendationScoredVideo -import dev.typetype.server.services.HomeRecommendationSourceTag -import dev.typetype.server.services.RecommendationFeedHistoryEntry -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class HomeRecommendationJitterTest { - @Test - fun `apply keeps candidate count and deterministic order size`() { - val scored = listOf( - scored("a", 1.0), - scored("b", 0.9), - scored("c", 0.8), - ) - val history = mapOf( - "https://yt.com/v/a" to RecommendationFeedHistoryEntry(4, System.currentTimeMillis()), - "https://yt.com/v/b" to RecommendationFeedHistoryEntry(2, System.currentTimeMillis()), - ) - val out = HomeRecommendationJitter.apply(scored, history) - assertEquals(3, out.size) - } - - private fun scored(id: String, score: Double): HomeRecommendationScoredVideo = HomeRecommendationScoredVideo( - video = VideoItem( - id = id, - title = id, - url = "https://yt.com/v/$id", - thumbnailUrl = "", - uploaderName = "u$id", - uploaderUrl = "https://yt.com/c/u$id", - uploaderAvatarUrl = "", - duration = 10, - viewCount = 0, - uploadDate = "", - uploaded = 0, - streamType = "video_stream", - isShortFormContent = false, - uploaderVerified = false, - shortDescription = null, - ), - score = score, - source = HomeRecommendationSourceTag.DISCOVERY_TRENDING, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationMetricsRoutesTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationMetricsRoutesTest.kt deleted file mode 100644 index b5f54e8..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationMetricsRoutesTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.cache.CacheService -import dev.typetype.server.models.ExtractionResult -import dev.typetype.server.models.SearchPageResponse -import dev.typetype.server.models.VideoItem -import dev.typetype.server.routes.homeRecommendationMetricsRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.RecommendationPrivacyService -import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService -import dev.typetype.server.services.SubscriptionsService -import dev.typetype.server.services.TrendingService -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import io.mockk.coEvery -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class HomeRecommendationMetricsRoutesTest { - private val cache: CacheService = mockk() - private val channelService: ChannelService = mockk() - private val trendingService: TrendingService = mockk() - private val searchService: SearchService = mockk() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) - private val resolverDeps = homeResolverDependencies( - subscriptions = SubscriptionsService(), - channelService = channelService, - cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, - trendingService = trendingService, - searchService = searchService, - ) - private val service = HomeRecommendationService( - poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, - ) - private val auth = AuthService.fixed(TEST_USER_ID) - - companion object { @BeforeAll @JvmStatic fun initDb() = TestDatabase.setup() } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - coEvery { cache.get(any()) } returns null - coEvery { cache.set(any(), any(), any()) } returns Unit - coEvery { searchService.search(any(), any(), any()) } returns ExtractionResult.Success( - SearchPageResponse(emptyList(), null, null, false), - ) - coEvery { trendingService.getTrending(any()) } returns ExtractionResult.Success( - listOf(video("a", "c1"), video("b", "c2"), video("a", "c1")), - ) - } - - @Test - fun `metrics route returns 400 without clicked urls`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { homeRecommendationMetricsRoutes(service, auth) } - } - val response = client.get("/recommendations/home/metrics") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } - - @Test - fun `metrics route returns evaluation payload`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { homeRecommendationMetricsRoutes(service, auth) } - } - val response = client.get("/recommendations/home/metrics?clicked=https://yt.com/v/a") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.OK, response.status) - val body = response.bodyAsText() - assertTrue(body.contains("ndcgAt10")) - assertTrue(body.contains("diversityAt10")) - assertTrue(body.contains("duplicateRateAt10")) - } - - private fun video(id: String, channel: String): VideoItem = VideoItem( - id = id, - title = id, - url = "https://yt.com/v/$id", - thumbnailUrl = "", - uploaderName = channel, - uploaderUrl = "https://yt.com/c/$channel", - uploaderAvatarUrl = "", - duration = 60, - viewCount = 0, - uploadDate = "", - uploaded = 1L, - streamType = "video_stream", - isShortFormContent = false, - uploaderVerified = false, - shortDescription = null, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationOfflineEvaluatorTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationOfflineEvaluatorTest.kt deleted file mode 100644 index 19434ae..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationOfflineEvaluatorTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.HomeRecommendationOfflineEvaluator -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationOfflineEvaluatorTest { - @Test - fun `evaluate returns bounded metrics`() { - val ranked = listOf( - video("a", "c1"), - video("b", "c2"), - video("c", "c3"), - video("a", "c1"), - ) - val metrics = HomeRecommendationOfflineEvaluator.evaluate( - ranked = ranked, - clickedUrls = setOf("https://yt.com/v/a", "https://yt.com/v/c"), - ) - assertTrue(metrics.ndcgAt10 in 0.0..1.0) - assertEquals(0.75, metrics.diversityAt10) - assertEquals(0.25, metrics.duplicateRateAt10) - } - - @Test - fun `evaluate empty ranked list returns zero metrics`() { - val metrics = HomeRecommendationOfflineEvaluator.evaluate(emptyList(), setOf("https://yt.com/v/a")) - assertEquals(0.0, metrics.ndcgAt10) - assertEquals(0.0, metrics.diversityAt10) - assertEquals(0.0, metrics.duplicateRateAt10) - } - - private fun video(id: String, channel: String): VideoItem = VideoItem( - id = id, - title = id, - url = "https://yt.com/v/$id", - thumbnailUrl = "", - uploaderName = channel, - uploaderUrl = "https://yt.com/c/$channel", - uploaderAvatarUrl = "", - duration = 60, - viewCount = 0, - uploadDate = "", - uploaded = 1L, - streamType = "video_stream", - isShortFormContent = false, - uploaderVerified = false, - shortDescription = null, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationPersonalizationTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationPersonalizationTest.kt deleted file mode 100644 index e1bc36a..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationPersonalizationTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.HomeRecommendationPersonalization -import dev.typetype.server.services.HomeRecommendationProfile -import dev.typetype.server.services.RecommendationFeedHistoryEntry -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationPersonalizationTest { - @Test - fun `feed history penalizes repeated shown video`() { - val now = System.currentTimeMillis() - val profile = HomeRecommendationProfile( - seenUrls = emptySet(), - blockedVideos = emptySet(), - blockedChannels = emptySet(), - feedbackBlockedVideos = emptySet(), - feedbackBlockedChannels = emptySet(), - subscriptionChannels = emptySet(), - favoriteUrls = emptySet(), - watchLaterUrls = emptySet(), - keywordAffinity = emptySet(), - themeTokens = emptySet(), - themeQueries = emptyList(), - feedHistory = mapOf("https://yt.com/v/a" to RecommendationFeedHistoryEntry(showCount = 5, lastShown = now - 1_000L)), - rejectionTopicPenalty = emptyMap(), - rejectionChannelPenalty = emptyMap(), - channelTopicProfile = emptyMap(), - shortsTopicInterest = emptyMap(), - ) - val scored = HomeRecommendationPersonalization.applyPenalties(video("a"), score = 2.0, profile = profile) - assertTrue(scored < 1.0) - } - - private fun video(id: String): VideoItem = VideoItem( - id = id, - title = "title", - url = "https://yt.com/v/$id", - thumbnailUrl = "", - uploaderName = "uploader", - uploaderUrl = "https://yt.com/c/uploader", - uploaderAvatarUrl = "", - duration = 60, - viewCount = 2_000, - uploadDate = "", - uploaded = System.currentTimeMillis() - 3_600_000L, - streamType = "video_stream", - isShortFormContent = false, - uploaderVerified = false, - shortDescription = null, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderPersonalizationOffTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderPersonalizationOffTest.kt deleted file mode 100644 index 68f2672..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderPersonalizationOffTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.HomeRecommendationPoolBuilder -import dev.typetype.server.services.HomeRecommendationDeviceClass -import dev.typetype.server.services.HomeRecommendationProfile -import dev.typetype.server.services.HomeRecommendationSessionContext -import dev.typetype.server.services.HomeRecommendationSessionIntent -import dev.typetype.server.services.HomeRecommendationSourceTag -import dev.typetype.server.services.HomeRecommendationTaggedVideo -import dev.typetype.server.services.RecommendationFeedHistoryEntry -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationPoolBuilderPersonalizationOffTest { - @Test - fun `pool builder ignores personalization penalties when kill switch disabled`() { - val video = video("a", "channel") - val profile = HomeRecommendationProfile( - seenUrls = emptySet(), - blockedVideos = emptySet(), - blockedChannels = emptySet(), - feedbackBlockedVideos = emptySet(), - feedbackBlockedChannels = emptySet(), - subscriptionChannels = emptySet(), - favoriteUrls = emptySet(), - watchLaterUrls = emptySet(), - keywordAffinity = emptySet(), - themeTokens = emptySet(), - themeQueries = emptyList(), - feedHistory = mapOf(video.url to RecommendationFeedHistoryEntry(showCount = 8, lastShown = System.currentTimeMillis())), - rejectionTopicPenalty = mapOf("title" to 0.2), - rejectionChannelPenalty = mapOf(video.uploaderUrl to 0.2), - personalizationEnabled = false, - ) - val pool = HomeRecommendationPoolBuilder().build( - profile = profile, - subscriptionCandidates = listOf(HomeRecommendationTaggedVideo(video, HomeRecommendationSourceTag.SUBSCRIPTION)), - discoveryCandidates = emptyList(), - context = context, - ) - assertTrue(pool.subscriptions.isNotEmpty()) - } - - private fun video(id: String, channel: String): VideoItem = VideoItem( - id = id, - title = "title $id", - url = "https://yt.com/v/$id", - thumbnailUrl = "", - uploaderName = channel, - uploaderUrl = "https://yt.com/c/$channel", - uploaderAvatarUrl = "", - duration = 60, - viewCount = 0, - uploadDate = "", - uploaded = System.currentTimeMillis(), - streamType = "video_stream", - isShortFormContent = false, - uploaderVerified = false, - shortDescription = null, - ) - - private val context = HomeRecommendationSessionContext( - intent = HomeRecommendationSessionIntent.AUTO, - deviceClass = HomeRecommendationDeviceClass.UNKNOWN, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderTest.kt index 12ef152..225b18d 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolBuilderTest.kt @@ -6,7 +6,6 @@ import dev.typetype.server.services.HomeRecommendationSessionContext import dev.typetype.server.services.HomeRecommendationSessionIntent import dev.typetype.server.services.HomeRecommendationDeviceClass import dev.typetype.server.services.HomeRecommendationProfile -import dev.typetype.server.services.HomeRecommendationScoring import dev.typetype.server.services.HomeRecommendationSourceTag import dev.typetype.server.services.HomeRecommendationTaggedVideo import org.junit.jupiter.api.Assertions.assertEquals @@ -73,37 +72,6 @@ class HomeRecommendationPoolBuilderTest { assertEquals("https://yt.com/v/new", pool.discovery.first().url) } - @Test - fun `pool builder ranks title keyword matches higher`() { - val profile = HomeRecommendationProfile( - seenUrls = emptySet(), - blockedVideos = emptySet(), - blockedChannels = emptySet(), - feedbackBlockedVideos = emptySet(), - feedbackBlockedChannels = emptySet(), - subscriptionChannels = emptySet(), - favoriteUrls = emptySet(), - watchLaterUrls = emptySet(), - keywordAffinity = setOf("music"), - themeTokens = emptySet(), - themeQueries = emptyList(), - channelInterest = emptyMap(), - topicInterest = emptyMap(), - feedHistory = emptyMap(), - rejectionTopicPenalty = emptyMap(), - rejectionChannelPenalty = emptyMap(), - channelTopicProfile = emptyMap(), - shortsTopicInterest = emptyMap(), - ) - val subscriptions = listOf( - tagged(video("plain", "a", title = "quantum lattice"), HomeRecommendationSourceTag.SUBSCRIPTION), - tagged(video("match", "a", title = "music lattice"), HomeRecommendationSourceTag.SUBSCRIPTION), - ) - val plainScore = HomeRecommendationScoring.scoreSubscription(subscriptions[0].video, profile, context) - val matchScore = HomeRecommendationScoring.scoreSubscription(subscriptions[1].video, profile, context) - assertTrue(matchScore > plainScore) - } - @Test fun `pool builder drops live-like discovery candidates`() { val profile = HomeRecommendationProfile( diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolResolverCacheKeyTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolResolverCacheKeyTest.kt deleted file mode 100644 index c6c4c5f..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationPoolResolverCacheKeyTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.cache.CacheService -import dev.typetype.server.models.ExtractionResult -import dev.typetype.server.models.SearchPageResponse -import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.HomeRecommendationPoolResolver -import dev.typetype.server.services.HomeRecommendationPoolMode -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.HomeRecommendationContext -import dev.typetype.server.services.HomeRecommendationDeviceClass -import dev.typetype.server.services.HomeRecommendationSessionContext -import dev.typetype.server.services.HomeRecommendationSessionIntent -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SubscriptionsService -import dev.typetype.server.services.TrendingService -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertNotEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class HomeRecommendationPoolResolverCacheKeyTest { - private val cache: CacheService = mockk() - private val channelService: ChannelService = mockk() - private val trendingService: TrendingService = mockk() - private val searchService: SearchService = mockk() - private val subscriptions = SubscriptionsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val resolverDeps = homeResolverDependencies( - subscriptions = subscriptions, - channelService = channelService, - cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = RecommendationFeedHistoryService(), - trendingService = trendingService, - searchService = searchService, - ) - private val resolver = buildHomeResolver(resolverDeps) - - companion object { @BeforeAll @JvmStatic fun initDb() = TestDatabase.setup() } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - coEvery { cache.get(any()) } returns null - coEvery { cache.set(any(), any(), any()) } returns Unit - coEvery { trendingService.getTrending(any()) } returns ExtractionResult.Success(emptyList()) - coEvery { searchService.search(any(), any(), any()) } returns ExtractionResult.Success(SearchPageResponse(emptyList(), null, null, false)) - } - - @Test - fun `cache key differs when personalization flag differs`() = runTest { - val homeKeys = mutableListOf() - coEvery { cache.get(any()) } answers { - val key = firstArg() - if (key.startsWith("recommendations:home:")) homeKeys += key - null - } - resolver.resolve(TEST_USER_ID, 0, HomeRecommendationPoolMode.FULL, personalizationEnabled = true, context = context()) - resolver.resolve(TEST_USER_ID, 0, HomeRecommendationPoolMode.FULL, personalizationEnabled = false, context = context()) - assertNotEquals(homeKeys.firstOrNull(), homeKeys.lastOrNull()) - } - - private fun context() = defaultContext() -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesKillSwitchTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesKillSwitchTest.kt deleted file mode 100644 index 26a01cc..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesKillSwitchTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.cache.CacheService -import dev.typetype.server.models.ExtractionResult -import dev.typetype.server.models.SearchPageResponse -import dev.typetype.server.models.SettingsItem -import dev.typetype.server.routes.homeRecommendationRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.BlockedService -import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.FavoritesService -import dev.typetype.server.services.HomeRecommendationPoolResolver -import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.RecommendationPrivacyService -import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService -import dev.typetype.server.services.SubscriptionsService -import dev.typetype.server.services.TrendingService -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import io.mockk.coEvery -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class HomeRecommendationRoutesKillSwitchTest { - private val cache: CacheService = mockk() - private val channelService: ChannelService = mockk() - private val trendingService: TrendingService = mockk() - private val searchService: SearchService = mockk() - private val settings = SettingsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistory = RecommendationFeedHistoryService() - private val resolverDeps = homeResolverDependencies( - subscriptions = SubscriptionsService(), - channelService = channelService, - cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistory, - trendingService = trendingService, - searchService = searchService, - ) - private val service = HomeRecommendationService( - poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistory, - privacyService = RecommendationPrivacyService(settings), - ) - private val auth = AuthService.fixed(TEST_USER_ID) - - companion object { @BeforeAll @JvmStatic fun initDb() = TestDatabase.setup() } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - kotlinx.coroutines.runBlocking { - settings.upsert(TEST_USER_ID, SettingsItem(recommendationPersonalizationEnabled = false)) - } - coEvery { cache.get(any()) } returns null - coEvery { cache.set(any(), any(), any()) } returns Unit - coEvery { trendingService.getTrending(any()) } returns ExtractionResult.Success(emptyList()) - coEvery { searchService.search(any(), any(), any()) } returns ExtractionResult.Success(SearchPageResponse(emptyList(), null, null, false)) - } - - @Test - fun `home route still returns 200 when personalization disabled`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { homeRecommendationRoutes(service, auth) } - } - val response = client.get("/recommendations/home") { - headers.append(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.OK, response.status) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesTest.kt index fcf62b6..8c9a12e 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesTest.kt @@ -8,19 +8,9 @@ import dev.typetype.server.models.SubscriptionItem import dev.typetype.server.models.VideoItem import dev.typetype.server.routes.homeRecommendationRoutes import dev.typetype.server.services.AuthService -import dev.typetype.server.services.BlockedService import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.FavoritesService -import dev.typetype.server.services.HistoryService import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.HomeRecommendationPoolResolver -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationPrivacyService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -45,25 +35,15 @@ class HomeRecommendationRoutesTest { private val channelService: ChannelService = mockk() private val trendingService: TrendingService = mockk() private val searchService: SearchService = mockk() - private val feedback = RecommendationFeedbackService(RecommendationEventService(RecommendationInterestService())) - private val eventService = RecommendationEventService(RecommendationInterestService()) private val subscriptions = SubscriptionsService() - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) private val resolverDeps = homeResolverDependencies( subscriptions = subscriptions, channelService = channelService, cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, trendingService = trendingService, - searchService = searchService, ) private val service = HomeRecommendationService( poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesValidationTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesValidationTest.kt index 3e94938..ef32422 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesValidationTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationRoutesValidationTest.kt @@ -5,19 +5,9 @@ import dev.typetype.server.models.ExtractionResult import dev.typetype.server.models.SearchPageResponse import dev.typetype.server.routes.homeRecommendationRoutes import dev.typetype.server.services.AuthService -import dev.typetype.server.services.BlockedService import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.FavoritesService -import dev.typetype.server.services.HistoryService -import dev.typetype.server.services.HomeRecommendationPoolResolver import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationPrivacyService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -42,24 +32,14 @@ class HomeRecommendationRoutesValidationTest { private val channelService: ChannelService = mockk() private val trendingService: TrendingService = mockk() private val searchService: SearchService = mockk() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) private val resolverDeps = homeResolverDependencies( subscriptions = SubscriptionsService(), channelService = channelService, cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, trendingService = trendingService, - searchService = searchService, ) private val service = HomeRecommendationService( poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationServiceFastPathTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationServiceFastPathTest.kt index bc47b9b..9c324a5 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationServiceFastPathTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationServiceFastPathTest.kt @@ -7,20 +7,10 @@ import dev.typetype.server.models.ExtractionResult import dev.typetype.server.models.SearchPageResponse import dev.typetype.server.models.SubscriptionItem import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.BlockedService import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.FavoritesService -import dev.typetype.server.services.HistoryService import dev.typetype.server.services.HomeRecommendationCursor -import dev.typetype.server.services.HomeRecommendationPoolResolver import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationPrivacyService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.mockk.coEvery @@ -39,24 +29,14 @@ class HomeRecommendationServiceFastPathTest { private val trendingService: TrendingService = mockk() private val searchService: SearchService = mockk() private val subscriptions = SubscriptionsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) private val resolverDeps = homeResolverDependencies( subscriptions = subscriptions, channelService = channelService, cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, trendingService = trendingService, - searchService = searchService, ) private val service = HomeRecommendationService( poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) companion object { @BeforeAll @JvmStatic fun initDb() = TestDatabase.setup() } diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsDebugRoutesTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsDebugRoutesTest.kt index 92d126e..821bd99 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsDebugRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsDebugRoutesTest.kt @@ -8,13 +8,7 @@ import dev.typetype.server.routes.homeRecommendationShortsRoutes import dev.typetype.server.services.AuthService import dev.typetype.server.services.ChannelService import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.RecommendationPrivacyService import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -40,24 +34,14 @@ class HomeRecommendationShortsDebugRoutesTest { private val channelService: ChannelService = mockk() private val trendingService: TrendingService = mockk() private val searchService: SearchService = mockk() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) private val resolverDeps = homeResolverDependencies( subscriptions = SubscriptionsService(), channelService = channelService, cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, trendingService = trendingService, - searchService = searchService, ) private val service = HomeRecommendationService( poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsProfileFitTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsProfileFitTest.kt deleted file mode 100644 index f3f8f2c..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsProfileFitTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.VideoItem -import dev.typetype.server.services.HomeRecommendationProfile -import dev.typetype.server.services.HomeRecommendationShortsProfileFit -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationShortsProfileFitTest { - @Test - fun `profile fit favors subscribed channel over generic viral`() { - val profile = HomeRecommendationProfile( - seenUrls = emptySet(), - blockedVideos = emptySet(), - blockedChannels = emptySet(), - feedbackBlockedVideos = emptySet(), - feedbackBlockedChannels = emptySet(), - subscriptionChannels = setOf("https://yt.com/c/linux"), - favoriteUrls = emptySet(), - watchLaterUrls = emptySet(), - keywordAffinity = setOf("linux", "kernel", "terminal"), - themeTokens = emptySet(), - themeQueries = emptyList(), - ) - val subscribed = video("linux terminal tips", "https://yt.com/c/linux") - val generic = video("random viral facts", "https://yt.com/c/random") - val a = HomeRecommendationShortsProfileFit.score(subscribed, profile) - val b = HomeRecommendationShortsProfileFit.score(generic, profile) - assertTrue(a > b) - } - - private fun video(title: String, uploaderUrl: String): VideoItem = VideoItem( - id = title, - title = title, - url = "https://yt.com/v/${title.hashCode()}", - thumbnailUrl = "", - uploaderName = "u", - uploaderUrl = uploaderUrl, - uploaderAvatarUrl = "", - duration = 40, - viewCount = 0, - uploadDate = "", - uploaded = System.currentTimeMillis(), - streamType = "video_stream", - isShortFormContent = true, - uploaderVerified = false, - shortDescription = null, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsQueryFactoryTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsQueryFactoryTest.kt deleted file mode 100644 index 63f99ae..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsQueryFactoryTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.services.HomeRecommendationProfile -import dev.typetype.server.services.HomeRecommendationShortsQueryFactory -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationShortsQueryFactoryTest { - @Test - fun `query factory filters generic keywords and keeps profile terms`() { - val profile = HomeRecommendationProfile( - seenUrls = emptySet(), - blockedVideos = emptySet(), - blockedChannels = emptySet(), - feedbackBlockedVideos = emptySet(), - feedbackBlockedChannels = emptySet(), - subscriptionChannels = emptySet(), - favoriteUrls = emptySet(), - watchLaterUrls = emptySet(), - keywordAffinity = setOf("linux", "shorts", "random", "kernel"), - themeTokens = emptySet(), - themeQueries = listOf("linux desktop"), - ) - val queries = HomeRecommendationShortsQueryFactory.fromProfile(profile, limit = 6) - assertTrue(queries.any { it.contains("linux") || it.contains("kernel") }) - assertTrue(queries.none { it == "random shorts" }) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsRoutesTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsRoutesTest.kt index b9f675d..7ff8095 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsRoutesTest.kt @@ -8,13 +8,7 @@ import dev.typetype.server.routes.homeRecommendationShortsRoutes import dev.typetype.server.services.AuthService import dev.typetype.server.services.ChannelService import dev.typetype.server.services.HomeRecommendationService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.RecommendationPrivacyService import dev.typetype.server.services.SearchService -import dev.typetype.server.services.SettingsService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -41,24 +35,14 @@ class HomeRecommendationShortsRoutesTest { private val channelService: ChannelService = mockk() private val trendingService: TrendingService = mockk() private val searchService: SearchService = mockk() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedback = RecommendationFeedbackService(eventService) - private val feedHistoryService = RecommendationFeedHistoryService() - private val privacyService = RecommendationPrivacyService(SettingsService()) private val resolverDeps = homeResolverDependencies( subscriptions = SubscriptionsService(), channelService = channelService, cache = cache, - feedbackService = feedback, - eventService = eventService, - feedHistoryService = feedHistoryService, trendingService = trendingService, - searchService = searchService, ) private val service = HomeRecommendationService( poolResolver = buildHomeResolver(resolverDeps), - feedHistoryService = feedHistoryService, - privacyService = privacyService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSeenMemoryTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSeenMemoryTest.kt deleted file mode 100644 index 2ee8545..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSeenMemoryTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.services.HomeRecommendationShortsSeenMemory -import dev.typetype.server.services.RecommendationFeedHistoryEntry -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationShortsSeenMemoryTest { - @Test - fun `seen memory applies stronger penalty for repeated recent shorts`() { - val now = System.currentTimeMillis() - val light = HomeRecommendationShortsSeenMemory.penalty( - RecommendationFeedHistoryEntry(showCount = 1, lastShown = now - 30L * 60L * 1000L), - now = now, - ) - val heavy = HomeRecommendationShortsSeenMemory.penalty( - RecommendationFeedHistoryEntry(showCount = 4, lastShown = now - 30L * 60L * 1000L), - now = now, - ) - assertTrue(heavy < light) - } -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSignalsTest.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSignalsTest.kt deleted file mode 100644 index b53b6da..0000000 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationShortsSignalsTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.RecommendationEventItem -import dev.typetype.server.services.HomeRecommendationShortsSignals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class HomeRecommendationShortsSignalsTest { - @Test - fun `signals penalize repeated instant skips more than late skips`() { - val urlA = "https://www.youtube.com/shorts/a" - val urlB = "https://www.youtube.com/shorts/b" - val now = System.currentTimeMillis() - val events = listOf( - event(urlA, 300, now), - event(urlA, 450, now), - event(urlA, 600, now), - event(urlB, 5000, now), - ) - val penalties = HomeRecommendationShortsSignals.shortSkipPenaltyByUrl(events) - assertTrue((penalties[urlA] ?: 1.0) < (penalties[urlB] ?: 1.0)) - } - - private fun event(url: String, duration: Long, now: Long): RecommendationEventItem = RecommendationEventItem( - id = "id-$url-$duration", - eventType = "short_skip", - videoUrl = url, - uploaderUrl = null, - title = "short", - watchRatio = null, - watchDurationMs = duration, - contextKey = "0:we:afternoon:quick:unknown", - occurredAt = now, - ) -} diff --git a/src/test/kotlin/dev/typetype/server/HomeRecommendationTestFixtures.kt b/src/test/kotlin/dev/typetype/server/HomeRecommendationTestFixtures.kt index 838d7f7..49ce9ff 100644 --- a/src/test/kotlin/dev/typetype/server/HomeRecommendationTestFixtures.kt +++ b/src/test/kotlin/dev/typetype/server/HomeRecommendationTestFixtures.kt @@ -11,16 +11,10 @@ import dev.typetype.server.services.HomeRecommendationPoolResolver import dev.typetype.server.services.HomeRecommendationPoolResolverDependencies import dev.typetype.server.services.HomeRecommendationSessionContext import dev.typetype.server.services.HomeRecommendationSessionIntent -import dev.typetype.server.services.PlaylistService import dev.typetype.server.services.HomeRecommendationSignalContextService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedHistoryService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.SearchService import dev.typetype.server.services.SubscriptionFeedService import dev.typetype.server.services.SubscriptionShortsBlendService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.SubscriptionShortsSignalService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import dev.typetype.server.services.WatchLaterService @@ -29,31 +23,21 @@ fun homeResolverDependencies( subscriptions: SubscriptionsService, channelService: ChannelService, cache: CacheService, - feedbackService: RecommendationFeedbackService, - eventService: RecommendationEventService, - feedHistoryService: RecommendationFeedHistoryService, trendingService: TrendingService, - searchService: SearchService, ): HomeRecommendationPoolResolverDependencies = HomeRecommendationPoolResolverDependencies( subscriptionsService = subscriptions, subscriptionFeedService = SubscriptionFeedService(subscriptions, channelService, cache), subscriptionShortsFeedService = SubscriptionShortsFeedService( subscriptions, channelService, - SubscriptionShortsBlendService(trendingService, SubscriptionShortsSignalService(eventService)), + SubscriptionShortsBlendService(trendingService), cache, ), historyService = HistoryService(), - playlistService = PlaylistService(), favoritesService = FavoritesService(), watchLaterService = WatchLaterService(), blockedService = BlockedService(), - feedbackService = feedbackService, - eventService = eventService, - feedHistoryService = feedHistoryService, signalContextService = HomeRecommendationSignalContextService(subscriptions, HistoryService()), - trendingService = trendingService, - searchService = searchService, cache = cache, ) diff --git a/src/test/kotlin/dev/typetype/server/RecommendationEventItemSerializationTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationEventItemSerializationTest.kt deleted file mode 100644 index 82f5232..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationEventItemSerializationTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.RecommendationEventItem -import kotlinx.serialization.json.Json -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -private val serializationJson: Json = Json { encodeDefaults = true } - -class RecommendationEventItemSerializationTest { - @Test - fun `serialization includes publishedAt`() { - val item = RecommendationEventItem( - id = "id", - eventType = "click", - videoUrl = "https://yt.com/v/a", - uploaderUrl = null, - title = null, - watchRatio = null, - watchDurationMs = null, - contextKey = null, - occurredAt = 1_700_000_000_123L, - publishedAt = 1_700_000_000_123L, - ) - val json = serializationJson.encodeToString(RecommendationEventItem.serializer(), item) - assertTrue(json.contains("\"publishedAt\":1700000000123")) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationEventPrivacyTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationEventPrivacyTest.kt deleted file mode 100644 index a1fe451..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationEventPrivacyTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.SettingsItem -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.SettingsService -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationEventPrivacyTest { - private val settings = SettingsService() - private val service = RecommendationEventService(RecommendationInterestService()) - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `event add is no-op when personalization disabled`() = kotlinx.coroutines.test.runTest { - settings.upsert(TEST_USER_ID, SettingsItem(recommendationPersonalizationEnabled = false)) - service.add( - userId = TEST_USER_ID, - eventType = "click", - videoUrl = "https://yt.com/v/a", - uploaderUrl = "https://yt.com/c/a", - title = "hello world", - watchRatio = null, - watchDurationMs = null, - ) - assertEquals(0, service.getAll(TEST_USER_ID).size) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationEventsRoutesTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationEventsRoutesTest.kt deleted file mode 100644 index 06b05b1..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationEventsRoutesTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.routes.recommendationEventsRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationEventsRoutesTest { - private val auth = AuthService.fixed(TEST_USER_ID) - private val service = RecommendationEventService(RecommendationInterestService()) - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `POST and GET recommendation events works`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val post = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"click","videoUrl":"https://yt.com/v/a","title":"linux release"}""") - } - assertEquals(HttpStatusCode.Created, post.status) - val get = client.get("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.OK, get.status) - assertTrue(get.bodyAsText().contains("click")) - } - - @Test - fun `POST invalid eventType returns 400`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val response = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"bad"}""") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } - - @Test - fun `POST short skip eventType is accepted`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val response = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"short_skip","videoUrl":"https://yt.com/v/skip"}""") - } - assertEquals(HttpStatusCode.Created, response.status) - } - - @Test - fun `POST short skip with negative watchDurationMs returns 400`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val response = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"short_skip","videoUrl":"https://yt.com/v/skip","watchDurationMs":-1}""") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } - - @Test - fun `POST event with long contextKey returns 400`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val contextKey = "x".repeat(121) - val response = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"click","videoUrl":"https://yt.com/v/ctx","contextKey":"$contextKey"}""") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } - - @Test - fun `POST event auto-derives context key when omitted`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationEventsRoutes(service, auth) } - } - val post = client.post("/recommendations/events") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"eventType":"click","videoUrl":"https://yt.com/v/ctx-auto","serviceId":0,"intent":"quick"}""") - } - assertEquals(HttpStatusCode.Created, post.status) - val get = client.get("/recommendations/events") { header(HttpHeaders.Authorization, "Bearer test-jwt") } - assertEquals(HttpStatusCode.OK, get.status) - assertTrue(get.bodyAsText().contains("ctx-auto")) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationFeedbackPrivacyTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationFeedbackPrivacyTest.kt deleted file mode 100644 index f195461..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationFeedbackPrivacyTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.models.SettingsItem -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.SettingsService -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationFeedbackPrivacyTest { - private val settings = SettingsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val feedbackService = RecommendationFeedbackService(eventService) - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `feedback add is no-op when personalization disabled`() = runTest { - settings.upsert(TEST_USER_ID, SettingsItem(recommendationPersonalizationEnabled = false)) - feedbackService.add(TEST_USER_ID, "not_interested", "https://yt.com/v/a", null) - assertEquals(0, feedbackService.getAll(TEST_USER_ID).size) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationFeedbackRoutesTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationFeedbackRoutesTest.kt deleted file mode 100644 index 9c775ea..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationFeedbackRoutesTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.routes.recommendationFeedbackRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationFeedbackService -import dev.typetype.server.services.RecommendationInterestService -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationFeedbackRoutesTest { - private val auth = AuthService.fixed(TEST_USER_ID) - private val service = RecommendationFeedbackService(RecommendationEventService(RecommendationInterestService())) - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `POST and GET recommendation feedback works`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationFeedbackRoutes(service, auth) } - } - val post = client.post("/recommendations/feedback") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"feedbackType":"not_interested","videoUrl":"https://yt.com/v/abc"}""") - } - assertEquals(HttpStatusCode.Created, post.status) - val get = client.get("/recommendations/feedback") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.OK, get.status) - assertTrue(get.bodyAsText().contains("not_interested")) - } - - @Test - fun `POST invalid feedbackType returns 400`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationFeedbackRoutes(service, auth) } - } - val response = client.post("/recommendations/feedback") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"feedbackType":"foo"}""") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationInterestServiceTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationInterestServiceTest.kt deleted file mode 100644 index a7900c9..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationInterestServiceTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.db.DatabaseFactory -import dev.typetype.server.db.tables.UserChannelInterestTable -import dev.typetype.server.db.tables.UserTopicInterestTable -import dev.typetype.server.services.RecommendationInterestService -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationInterestServiceTest { - private val service = RecommendationInterestService() - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `update upserts and accumulates channel and topics`() { - runSuspend { - service.update( - userId = TEST_USER_ID, - eventType = "watch", - uploaderUrl = "https://yt.com/c/a", - title = "linux kernel scheduler", - watchRatio = 0.8, - ) - service.update( - userId = TEST_USER_ID, - eventType = "watch", - uploaderUrl = "https://yt.com/c/a", - title = "linux kernel scheduler", - watchRatio = 0.8, - ) - } - - val channelScore = runSuspendWithResult { - DatabaseFactory.query { - UserChannelInterestTable.selectAll() - .where { (UserChannelInterestTable.userId eq TEST_USER_ID) and (UserChannelInterestTable.uploaderUrl eq "https://yt.com/c/a") } - .single()[UserChannelInterestTable.score] - } - } - assertEquals(6.0, channelScore) - - val topicScore = runSuspendWithResult { - DatabaseFactory.query { - UserTopicInterestTable.selectAll() - .where { (UserTopicInterestTable.userId eq TEST_USER_ID) and (UserTopicInterestTable.topic eq "linux") } - .single()[UserTopicInterestTable.score] - } - } - assertEquals(6.0, topicScore) - } - - private fun runSuspend(block: suspend () -> Unit) { - kotlinx.coroutines.runBlocking { block() } - } - - private fun runSuspendWithResult(block: suspend () -> T): T = - kotlinx.coroutines.runBlocking { block() } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationInterestWeightShortSkipTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationInterestWeightShortSkipTest.kt deleted file mode 100644 index e6f331c..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationInterestWeightShortSkipTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.services.RecommendationInterestWeight -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class RecommendationInterestWeightShortSkipTest { - @Test - fun `short skip maps to negative interest weight`() { - assertEquals(-1.0, RecommendationInterestWeight.of("short_skip", null)) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationOnboardingRoutesTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationOnboardingRoutesTest.kt deleted file mode 100644 index 52fe633..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationOnboardingRoutesTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.routes.recommendationOnboardingRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.RecommendationOnboardingService -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.put -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationOnboardingRoutesTest { - private val auth = AuthService.fixed(TEST_USER_ID) - private val service = RecommendationOnboardingService() - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `topics endpoint returns onboarding catalog`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - val response = client.get("/recommendations/onboarding/topics") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - } - assertEquals(HttpStatusCode.OK, response.status) - assertTrue(response.bodyAsText().contains("minTopics")) - assertTrue(response.bodyAsText().contains("Gaming")) - } - - @Test - fun `cannot complete onboarding before minimum topics`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - val response = client.post("/recommendations/onboarding/complete") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - } - - @Test - fun `save preferences then complete onboarding works`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - val save = client.put("/recommendations/onboarding/preferences") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"selectedTopics":["Linux","GTA","Hardware"],"selectedChannels":["https://www.youtube.com/channel/test"]}""") - } - assertEquals(HttpStatusCode.OK, save.status) - val complete = client.post("/recommendations/onboarding/complete") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - assertEquals(HttpStatusCode.OK, complete.status) - assertTrue(complete.bodyAsText().contains("\"requiresOnboarding\":false")) - } -} diff --git a/src/test/kotlin/dev/typetype/server/RecommendationOnboardingSkipReapplyRoutesTest.kt b/src/test/kotlin/dev/typetype/server/RecommendationOnboardingSkipReapplyRoutesTest.kt deleted file mode 100644 index 6e193ab..0000000 --- a/src/test/kotlin/dev/typetype/server/RecommendationOnboardingSkipReapplyRoutesTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.routes.recommendationOnboardingRoutes -import dev.typetype.server.services.AuthService -import dev.typetype.server.services.RecommendationOnboardingService -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.put -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class RecommendationOnboardingSkipReapplyRoutesTest { - private val auth = AuthService.fixed(TEST_USER_ID) - private val service = RecommendationOnboardingService() - - companion object { - @BeforeAll - @JvmStatic - fun initDb() = TestDatabase.setup() - } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `skip completes onboarding without minimum topics`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - val response = client.post("/recommendations/onboarding/skip") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - assertEquals(HttpStatusCode.OK, response.status) - val body = response.bodyAsText() - assertTrue(body.contains("\"requiresOnboarding\":false")) - assertTrue(body.contains("\"selectedTopics\":[]")) - } - - @Test - fun `reapply fails before onboarding completion`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - val response = client.post("/recommendations/onboarding/reapply") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - assertEquals(HttpStatusCode.BadRequest, response.status) - assertTrue(response.bodyAsText().contains("Complete onboarding before reapply")) - } - - @Test - fun `reapply works after onboarding completion`() = testApplication { - application { - install(ContentNegotiation) { json() } - routing { recommendationOnboardingRoutes(service, auth) } - } - client.put("/recommendations/onboarding/preferences") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("""{"selectedTopics":["Linux","GTA","Hardware"]}""") - } - client.post("/recommendations/onboarding/complete") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - val reapply = client.post("/recommendations/onboarding/reapply") { - header(HttpHeaders.Authorization, "Bearer test-jwt") - contentType(ContentType.Application.Json) - setBody("{}") - } - assertEquals(HttpStatusCode.OK, reapply.status) - assertTrue(reapply.bodyAsText().contains("\"requiresOnboarding\":false")) - } -} diff --git a/src/test/kotlin/dev/typetype/server/SettingsRoutesTest.kt b/src/test/kotlin/dev/typetype/server/SettingsRoutesTest.kt index 447d7c9..3ef4b94 100644 --- a/src/test/kotlin/dev/typetype/server/SettingsRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/SettingsRoutesTest.kt @@ -95,7 +95,7 @@ class SettingsRoutesTest { assertTrue(body.contains("\"defaultSubtitleLanguage\":\"\"")) assertTrue(body.contains("\"defaultAudioLanguage\":\"\"")) assertTrue(body.contains("\"preferOriginalLanguage\":false")) - assertTrue(body.contains("\"recommendationPersonalizationEnabled\":true")) + assertTrue(!body.contains("recommendationPersonalizationEnabled")) assertTrue(!body.contains("subscriptionSyncInterval")) } @@ -104,14 +104,14 @@ class SettingsRoutesTest { client.put("/settings") { headers.append(HttpHeaders.Authorization, "Bearer test-jwt") headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - setBody("""{"defaultService":0,"defaultQuality":"1080p","autoplay":true,"volume":1.0,"muted":false,"subtitlesEnabled":true,"defaultSubtitleLanguage":"fr","defaultAudioLanguage":"fr","preferOriginalLanguage":true,"recommendationPersonalizationEnabled":false,"subscriptionSyncInterval":60}""") + setBody("""{"defaultService":0,"defaultQuality":"1080p","autoplay":true,"volume":1.0,"muted":false,"subtitlesEnabled":true,"defaultSubtitleLanguage":"fr","defaultAudioLanguage":"fr","preferOriginalLanguage":true,"subscriptionSyncInterval":60}""") } val body = client.get("/settings") { headers.append(HttpHeaders.Authorization, "Bearer test-jwt") }.bodyAsText() assertTrue(body.contains("\"subtitlesEnabled\":true")) assertTrue(body.contains("\"defaultSubtitleLanguage\":\"fr\"")) assertTrue(body.contains("\"defaultAudioLanguage\":\"fr\"")) assertTrue(body.contains("\"preferOriginalLanguage\":true")) - assertTrue(body.contains("\"recommendationPersonalizationEnabled\":false")) + assertTrue(!body.contains("recommendationPersonalizationEnabled")) assertTrue(!body.contains("subscriptionSyncInterval")) } diff --git a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedBlendTest.kt b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedBlendTest.kt index 258c5f0..459bedb 100644 --- a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedBlendTest.kt +++ b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedBlendTest.kt @@ -6,11 +6,8 @@ import dev.typetype.server.models.ExtractionResult import dev.typetype.server.models.SubscriptionItem import dev.typetype.server.models.VideoItem import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SubscriptionShortsBlendService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.SubscriptionShortsSignalService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.mockk.coEvery @@ -26,11 +23,10 @@ class SubscriptionShortsFeedBlendTest { private val trendingService: TrendingService = mockk() private val cacheService: CacheService = mockk() private val subscriptionsService = SubscriptionsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) private val service = SubscriptionShortsFeedService( subscriptionsService, channelService, - SubscriptionShortsBlendService(trendingService, SubscriptionShortsSignalService(eventService)), + SubscriptionShortsBlendService(trendingService), cacheService, ) @@ -57,23 +53,6 @@ class SubscriptionShortsFeedBlendTest { assertTrue(feed.videos.any { it.url.endsWith("/shorts/d1") || it.url.endsWith("/shorts/d2") }) } - @Test - fun `blended feed downranks skipped shorts`() = runTest { - subscriptionsService.add(TEST_USER_ID, SubscriptionItem("https://yt.com/c/sub", "Sub", "")) - coEvery { channelService.getChannel("https://yt.com/c/sub/shorts", null) } returns ExtractionResult.Success( - ChannelResponse("Sub", "", "", "", 0, false, listOf(video("s1", "https://yt.com/c/sub")), null), - ) - eventService.add(TEST_USER_ID, "short_skip", "https://www.youtube.com/shorts/d1", null, null, null, 300) - eventService.add(TEST_USER_ID, "short_skip", "https://www.youtube.com/shorts/d1", null, null, null, 300) - eventService.add(TEST_USER_ID, "short_skip", "https://www.youtube.com/shorts/d1", null, null, null, 300) - coEvery { trendingService.getTrending(0) } returns ExtractionResult.Success( - listOf(video("d1", "https://yt.com/c/discovery"), video("d2", "https://yt.com/c/discovery2")), - ) - val feed = service.getBlendedFeed(TEST_USER_ID, 0, 0, 3) - val joined = feed.videos.joinToString("|") { it.url } - assertTrue(joined.indexOf("/shorts/d2") <= joined.indexOf("/shorts/d1")) - } - @Test fun `blended feed pages discovery results without repeating first page`() = runTest { subscriptionsService.add(TEST_USER_ID, SubscriptionItem("https://yt.com/c/sub", "Sub", "")) diff --git a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedDiversityTest.kt b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedDiversityTest.kt index 5b18c19..62b974a 100644 --- a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedDiversityTest.kt +++ b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedDiversityTest.kt @@ -8,11 +8,8 @@ import dev.typetype.server.models.VideoItem import dev.typetype.server.routes.subscriptionShortsFeedRoutes import dev.typetype.server.services.AuthService import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SubscriptionShortsBlendService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.SubscriptionShortsSignalService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -37,11 +34,10 @@ class SubscriptionShortsFeedDiversityTest { private val cacheService: CacheService = mockk() private val trendingService: TrendingService = mockk() private val subscriptionsService = SubscriptionsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) private val feedService = SubscriptionShortsFeedService( subscriptionsService, channelService, - SubscriptionShortsBlendService(trendingService, SubscriptionShortsSignalService(eventService)), + SubscriptionShortsBlendService(trendingService), cacheService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedRoutesTest.kt b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedRoutesTest.kt index 827d97c..aefdcd4 100644 --- a/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/SubscriptionShortsFeedRoutesTest.kt @@ -8,11 +8,8 @@ import dev.typetype.server.models.VideoItem import dev.typetype.server.routes.subscriptionShortsFeedRoutes import dev.typetype.server.services.AuthService import dev.typetype.server.services.ChannelService -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService import dev.typetype.server.services.SubscriptionShortsBlendService import dev.typetype.server.services.SubscriptionShortsFeedService -import dev.typetype.server.services.SubscriptionShortsSignalService import dev.typetype.server.services.SubscriptionsService import dev.typetype.server.services.TrendingService import io.ktor.client.request.get @@ -39,11 +36,10 @@ class SubscriptionShortsFeedRoutesTest { private val cacheService: CacheService = mockk() private val trendingService: TrendingService = mockk() private val subscriptionsService = SubscriptionsService() - private val eventService = RecommendationEventService(RecommendationInterestService()) private val feedService = SubscriptionShortsFeedService( subscriptionsService, channelService, - SubscriptionShortsBlendService(trendingService, SubscriptionShortsSignalService(eventService)), + SubscriptionShortsBlendService(trendingService), cacheService, ) private val auth = AuthService.fixed(TEST_USER_ID) diff --git a/src/test/kotlin/dev/typetype/server/SubscriptionShortsSignalServiceTest.kt b/src/test/kotlin/dev/typetype/server/SubscriptionShortsSignalServiceTest.kt deleted file mode 100644 index df6943a..0000000 --- a/src/test/kotlin/dev/typetype/server/SubscriptionShortsSignalServiceTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.typetype.server - -import dev.typetype.server.services.RecommendationEventService -import dev.typetype.server.services.RecommendationInterestService -import dev.typetype.server.services.SubscriptionShortsSignalService -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class SubscriptionShortsSignalServiceTest { - private val eventService = RecommendationEventService(RecommendationInterestService()) - private val service = SubscriptionShortsSignalService(eventService) - - companion object { @BeforeAll @JvmStatic fun initDb() = TestDatabase.setup() } - - @BeforeEach - fun clean() { - TestDatabase.truncateAll() - } - - @Test - fun `instant skips penalize more than late skips`() = runTest { - val urlInstant = "https://www.youtube.com/shorts/instant" - val urlLate = "https://www.youtube.com/shorts/late" - eventService.add(TEST_USER_ID, "short_skip", urlInstant, null, null, null, 200) - eventService.add(TEST_USER_ID, "short_skip", urlLate, null, null, null, 8_000) - val penalties = service.load(TEST_USER_ID) - assertTrue((penalties[urlInstant] ?: 1.0) < (penalties[urlLate] ?: 1.0)) - } -} diff --git a/src/test/kotlin/dev/typetype/server/TestDatabase.kt b/src/test/kotlin/dev/typetype/server/TestDatabase.kt index 0bc64b2..0baf099 100644 --- a/src/test/kotlin/dev/typetype/server/TestDatabase.kt +++ b/src/test/kotlin/dev/typetype/server/TestDatabase.kt @@ -16,13 +16,6 @@ import dev.typetype.server.db.tables.SearchHistoryTable import dev.typetype.server.db.tables.SettingsTable import dev.typetype.server.db.tables.SessionsTable import dev.typetype.server.db.tables.SubscriptionsTable -import dev.typetype.server.db.tables.RecommendationFeedbackTable -import dev.typetype.server.db.tables.RecommendationEventsTable -import dev.typetype.server.db.tables.RecommendationFeedHistoryTable -import dev.typetype.server.db.tables.RecommendationOnboardingPreferencesTable -import dev.typetype.server.db.tables.RecommendationOnboardingStateTable -import dev.typetype.server.db.tables.UserChannelInterestTable -import dev.typetype.server.db.tables.UserTopicInterestTable import dev.typetype.server.db.tables.YoutubeTakeoutImportJobsTable import dev.typetype.server.db.tables.YoutubeTakeoutPlaylistKeysTable import dev.typetype.server.db.tables.UsersTable @@ -103,13 +96,6 @@ object TestDatabase { AdminSettingsTable.deleteAll() BlockedChannelsTable.deleteAll() BlockedVideosTable.deleteAll() - RecommendationFeedbackTable.deleteAll() - RecommendationEventsTable.deleteAll() - RecommendationFeedHistoryTable.deleteAll() - RecommendationOnboardingPreferencesTable.deleteAll() - RecommendationOnboardingStateTable.deleteAll() - UserChannelInterestTable.deleteAll() - UserTopicInterestTable.deleteAll() YoutubeTakeoutImportJobsTable.deleteAll() YoutubeTakeoutPlaylistKeysTable.deleteAll() BugReportsTable.deleteAll()