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)
|
|
|
|
|
}
|
|
|
|
|
}
|