package recommend import ( "encoding/json" "strings" "testing" ) func explainItem(t *testing.T, payload string) Item { t.Helper() items := Decode([]json.RawMessage{json.RawMessage(payload)}) if len(items) != 1 { t.Fatalf("payload did not decode to one item: %s", payload) } return items[0] } func explainProfile(t *testing.T, history ...string) Profile { t.Helper() items := make([]Item, 0, len(history)) for _, payload := range history { items = append(items, explainItem(t, payload)) } return BuildProfile(items, nil) } const thrillerHistory = `{ "Id":"h1","Name":"Sicario","Type":"Movie","Genres":["Thriller","Crime"], "Studios":[{"Name":"Lionsgate"}], "People":[{"Name":"Denis Villeneuve","Type":"Director"},{"Name":"Emily Blunt","Type":"Actor"}] }` func TestWhyNamesTheGenreTheViewerActuallyWatches(t *testing.T) { profile := explainProfile(t, thrillerHistory) candidate := explainItem(t, `{ "Id":"c1","Name":"Prisoners","Type":"Movie","Genres":["Thriller"], "People":[{"Name":"Denis Villeneuve","Type":"Director"}] }`) reasons := Why(profile, candidate, ReasonLimit) if len(reasons) == 0 || reasons[0] != "Because you watch Thriller" { t.Fatalf("expected the genre reason first, got %v", reasons) } if !strings.Contains(strings.Join(reasons, "|"), "Denis Villeneuve") { t.Fatalf("expected the shared director to be named, got %v", reasons) } } func TestWhyNeverClaimsAnAffinityThatIsNotThere(t *testing.T) { profile := explainProfile(t, thrillerHistory) candidate := explainItem(t, `{ "Id":"c2","Name":"Paddington","Type":"Movie","Genres":["Family"], "CommunityRating":8.2, "People":[{"Name":"Paul King","Type":"Director"}] }`) reasons := Why(profile, candidate, ReasonLimit) for _, reason := range reasons { if strings.HasPrefix(reason, "Because you watch") || strings.HasPrefix(reason, "You've watched") { t.Fatalf("invented a taste the profile does not hold: %v", reasons) } } if len(reasons) == 0 || reasons[0] != "Well rated (8.2)" { t.Fatalf("expected the catalogue fact to carry the strip, got %v", reasons) } } func TestWhyAlwaysSaysSomething(t *testing.T) { candidate := explainItem(t, `{"Id":"c3","Name":"Unknown","Type":"Movie","Genres":["Drama"]}`) reasons := Why(Profile{}, candidate, ReasonLimit) if len(reasons) != 1 || reasons[0] != "Drama from your library" { t.Fatalf("an empty profile should still explain the title, got %v", reasons) } } func TestWhyIsCappedAndOrdered(t *testing.T) { // A second, Thriller-only title so Thriller genuinely outweighs Crime — with one // history item they tie and the alphabetical tie-break would decide it. profile := explainProfile(t, thrillerHistory, `{ "Id":"h2","Name":"Nightcrawler","Type":"Movie","Genres":["Thriller"], "Studios":[{"Name":"Lionsgate"}], "People":[{"Name":"Emily Blunt","Type":"Actor"}] }`) candidate := explainItem(t, `{ "Id":"c4","Name":"Wind River","Type":"Movie","Genres":["Thriller","Crime"], "Studios":[{"Name":"Lionsgate"}],"CommunityRating":9.1,"ProductionYear":2017, "People":[{"Name":"Emily Blunt","Type":"Actor"}] }`) reasons := Why(profile, candidate, ReasonLimit) if len(reasons) != ReasonLimit { t.Fatalf("expected exactly %d reasons, got %v", ReasonLimit, reasons) } want := []string{ "Because you watch Thriller", "You've watched Emily Blunt before", "More from Lionsgate", } for i, reason := range want { if reasons[i] != reason { t.Fatalf("reason %d: want %q, got %q (%v)", i, reason, reasons[i], reasons) } } } func TestWhyIsStableAcrossCalls(t *testing.T) { profile := explainProfile(t, thrillerHistory) candidate := explainItem(t, `{ "Id":"c5","Name":"Hell or High Water","Type":"Movie","Genres":["Crime","Thriller"] }`) first := Why(profile, candidate, ReasonLimit) for i := 0; i < 20; i++ { if got := Why(profile, candidate, ReasonLimit); !equalStrings(first, got) { t.Fatalf("reasons changed between calls: %v then %v", first, got) } } } func equalStrings(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }