Files
memby/server/internal/recommend/ranker_test.go
T

172 lines
7.1 KiB
Go
Raw Normal View History

2026-08-02 22:10:19 +12:00
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")
}
}
2026-08-06 22:33:56 +12:00
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)
}
}
2026-08-02 22:10:19 +12:00
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)
}
}