package recommend import ( "encoding/json" "testing" "time" ) func rankedFixture(id, name, kind string, genres []string, minutes int) Item { raw, _ := json.Marshal(map[string]any{ "Id": id, "Name": name, "Type": kind, "Genres": genres, "RunTimeTicks": int64(minutes) * 600_000_000, }) item := Item{ ID: id, Name: name, Type: kind, Genres: genres, RunTimeTicks: int64(minutes) * 600_000_000, CommunityRating: 7, Raw: raw, } return item } func TestWeightedRankPersonalizesTwoUsersFromRepeatedCompletedPatterns(t *testing.T) { now := time.Date(2026, 7, 31, 20, 0, 0, 0, time.UTC) dramaHistory := []ViewingEvidence{ {Item: rankedFixture("d1", "Drama One", "Movie", []string{"Drama"}, 100), Completion: 1, OccurredAt: now.Add(-24 * time.Hour)}, {Item: rankedFixture("d2", "Drama Two", "Series", []string{"Drama"}, 45), Completion: .96, OccurredAt: now.Add(-48 * time.Hour)}, } comedyHistory := []ViewingEvidence{ {Item: rankedFixture("c1", "Comedy One", "Movie", []string{"Comedy"}, 95), Completion: 1, OccurredAt: now.Add(-24 * time.Hour)}, {Item: rankedFixture("c2", "Comedy Two", "Series", []string{"Comedy"}, 25), Completion: .98, OccurredAt: now.Add(-48 * time.Hour)}, } candidates := []Item{ rankedFixture("new-comedy", "A Comedy", "Movie", []string{"Comedy"}, 90), rankedFixture("new-drama", "A Drama", "Movie", []string{"Drama"}, 90), } cfg := DefaultWeightedConfig() drama := WeightedRank(BuildWeightedProfile(dramaHistory, now, time.UTC), candidates, nil, RankIntent{Now: now}, cfg, 10) comedy := WeightedRank(BuildWeightedProfile(comedyHistory, now, time.UTC), candidates, nil, RankIntent{Now: now}, cfg, 10) if drama[0].Item.ID != "new-drama" || comedy[0].Item.ID != "new-comedy" { t.Fatalf("personalized first items = drama:%s comedy:%s", drama[0].Item.ID, comedy[0].Item.ID) } if drama[0].Explanation.Components["genre"] <= 0 || len(drama[0].Explanation.Reasons) == 0 { t.Fatalf("missing explainable component scores: %#v", drama[0].Explanation) } } func TestWeightedRankRequiresRepeatedAffinityEvidence(t *testing.T) { now := time.Now() profile := BuildWeightedProfile([]ViewingEvidence{{ Item: rankedFixture("once", "One Accidental Start", "Movie", []string{"Horror"}, 90), Completion: .05, OccurredAt: now, }}, now, time.UTC) ranked := WeightedRank(profile, []Item{ rankedFixture("horror", "Horror", "Movie", []string{"Horror"}, 90), rankedFixture("other", "Other", "Movie", []string{"Drama"}, 90), }, nil, RankIntent{Now: now}, DefaultWeightedConfig(), 10) for _, value := range ranked { if value.Explanation.Components["genre"] != 0 { t.Fatalf("one event established genre affinity: %#v", value.Explanation.Components) } } } func TestOnboardingRatingCreatesImmediateMetadataAffinity(t *testing.T) { profile := WeightedProfile{} rated := rankedFixture( "rated", "Arrival", "Movie", []string{"Science Fiction", "Drama"}, 116, ) rated.Studios = []struct { Name string `json:"Name"` }{{Name: "Paramount"}} rated.People = []Person{ {Name: "Amy Adams", Type: "Actor"}, {Name: "Denis Villeneuve", Type: "Director"}, } profile.ApplyOnboardingRating(rated, 5, 2) candidate := rankedFixture( "candidate", "Another Arrival", "Movie", []string{"Science Fiction"}, 120, ) candidate.Studios = rated.Studios candidate.People = rated.People ranked := WeightedRank( profile, []Item{candidate}, nil, RankIntent{}, DefaultWeightedConfig(), 1, ) if len(ranked) != 1 { t.Fatal("rated metadata produced no candidate") } components := ranked[0].Explanation.Components for _, key := range []string{"genre", "studio", "actor", "director"} { if components[key] <= 0 { t.Fatalf("%s component = %.3f, want positive: %#v", key, components[key], components) } } if !profile.Seen[rated.ID] { t.Fatal("the explicitly rated title was not marked seen") } } func TestOnboardingActressesFeedActorAffinity(t *testing.T) { profile := WeightedProfile{} profile.ApplyOnboarding(OnboardingPreferences{Actresses: []string{"Amy Adams"}}, 2) got := profile.Actors["amy adams"] if got.Evidence != 2 || got.Weight <= 0 { t.Fatalf("actress affinity = %#v", got) } } func TestWeightedRankNewReleaseEligibilityPrecedesPersonalization(t *testing.T) { now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC) old := rankedFixture("old", "Perfect Old Match", "Movie", []string{"Drama"}, 90) old.PremiereDate = "2020-01-01T00:00:00Z" fresh := rankedFixture("fresh", "Fresh Adjacent", "Movie", []string{"Comedy"}, 90) fresh.PremiereDate = "2026-07-01T00:00:00Z" profile := BuildWeightedProfile([]ViewingEvidence{ {Item: rankedFixture("d1", "D1", "Movie", []string{"Drama"}, 90), Completion: 1}, {Item: rankedFixture("d2", "D2", "Movie", []string{"Drama"}, 90), Completion: 1}, }, now, time.UTC) ranked := WeightedRank(profile, []Item{old, fresh}, nil, RankIntent{ Now: now, NewReleasesOnly: true, }, DefaultWeightedConfig(), 10) if len(ranked) != 1 || ranked[0].Item.ID != "fresh" { t.Fatalf("new release eligibility = %#v", ranked) } } func TestWeightedRankExcludesNegativeAndPenalizesIgnoredImpressions(t *testing.T) { now := time.Now() profile := WeightedProfile{ Genres: map[string]Affinity{}, Studios: map[string]Affinity{}, Actors: map[string]Affinity{}, Directors: map[string]Affinity{}, Franchises: map[string]Affinity{}, RuntimeRanges: map[string]Affinity{}, AgeRatings: map[string]Affinity{}, ReleasePeriods: map[string]Affinity{}, ContentTypes: map[string]Affinity{}, Seen: map[string]bool{}, ExplicitPositive: map[string]bool{}, ExplicitNegative: map[string]bool{"blocked": true}, TypicalSessionMins: map[string]float64{}, SessionEvidence: map[string]int{}, } items := []Item{ rankedFixture("ignored", "Ignored", "Movie", nil, 90), rankedFixture("fresh", "Fresh", "Movie", nil, 90), rankedFixture("blocked", "Blocked", "Movie", nil, 90), } ranked := WeightedRank(profile, items, map[string]ItemExposure{ "ignored": {Impressions: 12}, }, RankIntent{Now: now}, DefaultWeightedConfig(), 10) if len(ranked) != 2 || ranked[0].Item.ID != "fresh" { t.Fatalf("fatigue/negative ordering = %#v", ranked) } } func TestWeightedRankSessionFitSupportsBeforeBedIntent(t *testing.T) { now := time.Date(2026, 7, 31, 23, 0, 0, 0, time.UTC) slot := currentContextSlot(now, time.UTC) profile := WeightedProfile{ Genres: map[string]Affinity{}, Studios: map[string]Affinity{}, Actors: map[string]Affinity{}, Directors: map[string]Affinity{}, Franchises: map[string]Affinity{}, RuntimeRanges: map[string]Affinity{}, AgeRatings: map[string]Affinity{}, ReleasePeriods: map[string]Affinity{}, ContentTypes: map[string]Affinity{}, Seen: map[string]bool{}, ExplicitPositive: map[string]bool{}, ExplicitNegative: map[string]bool{}, TypicalSessionMins: map[string]float64{slot: 28}, SessionEvidence: map[string]int{slot: 5}, } ranked := WeightedRank(profile, []Item{ rankedFixture("film", "Long Film", "Movie", nil, 125), rankedFixture("episode", "One Episode", "Series", nil, 27), }, nil, RankIntent{Now: now, Location: time.UTC, PreferShort: true}, DefaultWeightedConfig(), 10) if ranked[0].Item.ID != "episode" { t.Fatalf("bedtime ordering = %s", ranked[0].Item.ID) } }