Files

234 lines
7.5 KiB
Go
Raw Permalink Normal View History

2026-08-02 22:10:19 +12:00
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"}]
}`
2026-08-23 09:40:45 +12:00
func TestWhyPrefersTheRelatedTitleOverItsGenres(t *testing.T) {
2026-08-02 22:10:19 +12:00
profile := explainProfile(t, thrillerHistory)
candidate := explainItem(t, `{
2026-08-23 09:40:45 +12:00
"Id":"c1","Name":"Prisoners","Type":"Movie","Genres":["Thriller","Crime"],
2026-08-02 22:10:19 +12:00
"People":[{"Name":"Denis Villeneuve","Type":"Director"}]
}`)
reasons := Why(profile, candidate, ReasonLimit)
2026-08-23 09:40:45 +12:00
if len(reasons) == 0 || reasons[0] != "Because you watched Sicario" {
t.Fatalf("expected the related viewing reason first, got %v", reasons)
2026-08-02 22:10:19 +12:00
}
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)
2026-08-23 09:40:45 +12:00
if len(reasons) != 1 || reasons[0] != "Recommended from your library" {
2026-08-02 22:10:19 +12:00
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{
2026-08-23 09:40:45 +12:00
"Because you watched Sicario",
"Because you like crime thrillers",
"Because you watch Emily Blunt",
2026-08-02 22:10:19 +12:00
}
for i, reason := range want {
if reasons[i] != reason {
t.Fatalf("reason %d: want %q, got %q (%v)", i, reason, reasons[i], reasons)
}
}
}
2026-08-23 09:40:45 +12:00
func TestWhyDoesNotTreatDramaAloneAsATitleRelationship(t *testing.T) {
profile := explainProfile(t, `{
"Id":"h1","Name":"A Drama","Type":"Movie","Genres":["Drama"]
}`)
candidate := explainItem(t, `{
"Id":"c1","Name":"Another Drama","Type":"Movie","Genres":["Drama"]
}`)
reasons := Why(profile, candidate, ReasonLimit)
if reasons[0] != "Because you watch Drama" {
t.Fatalf("broad genre should be the fallback, got %v", reasons)
}
if strings.Contains(strings.Join(reasons, "|"), "A Drama") {
t.Fatalf("Drama alone invented a title relationship: %v", reasons)
}
}
func TestWhyUsesSpecificSubgenreBeforeBroadGenre(t *testing.T) {
profile := explainProfile(t, `{
"Id":"h1","Name":"Crime One","Type":"Movie","Genres":["Crime","Drama"]
}`)
// Do not retain title evidence here: this test isolates the learned theme wording.
profile.ReasonEvidence = nil
candidate := explainItem(t, `{
"Id":"c1","Name":"Crime Two","Type":"Series","Genres":["Drama","Crime"]
}`)
if got := Why(profile, candidate, 1); len(got) != 1 || got[0] != "Because you like crime dramas" {
t.Fatalf("specific theme reason = %v", got)
}
}
func TestWhyUsesCreatorBeforeBroadGenre(t *testing.T) {
profile := explainProfile(t, `{
"Id":"h1","Name":"The First","Type":"Movie","Genres":["Drama"],
"People":[{"Name":"Vince Gilligan","Type":"Writer"}]
}`)
candidate := explainItem(t, `{
"Id":"c1","Name":"The Second","Type":"Series","Genres":["Drama"],
"People":[{"Name":"Vince Gilligan","Type":"Writer"}]
}`)
if got := Why(profile, candidate, 1); len(got) != 1 || got[0] != "More from Vince Gilligan" {
t.Fatalf("creator reason = %v", got)
}
}
func TestWhyDistinguishesFinishedTitlesFromEpisodes(t *testing.T) {
finished := explainItem(t, `{
"Id":"h1","Name":"Breaking Bad","Type":"Series","Genres":["Crime","Drama"],
"UserData":{"Played":true}
}`)
candidate := explainItem(t, `{
"Id":"c1","Name":"Better Call Saul","Type":"Series","Genres":["Crime","Drama"]
}`)
if got := Why(BuildProfile([]Item{finished}, nil), candidate, 1); got[0] != "Because you finished Breaking Bad" {
t.Fatalf("completed title reason = %v", got)
}
episodeHistory := explainItem(t, `{
"Id":"ep1","Name":"Pilot","Type":"Episode","SeriesId":"show-1",
"SeriesName":"Breaking Bad","Genres":["Crime","Drama"],"UserData":{"Played":true}
}`)
if got := Why(BuildProfile([]Item{episodeHistory}, nil), candidate, 1); got[0] != "Because you watched Breaking Bad" {
t.Fatalf("completed episode overstated series completion: %v", got)
}
}
func TestWhyCanUseARepeatedViewingEra(t *testing.T) {
profile := explainProfile(t,
`{"Id":"h1","Name":"One","Type":"Movie","ProductionYear":2003,"Genres":["Drama"]}`,
`{"Id":"h2","Name":"Two","Type":"Movie","ProductionYear":2007,"Genres":["Drama"]}`,
)
profile.ReasonEvidence = nil
candidate := explainItem(t, `{
"Id":"c1","Name":"Three","Type":"Movie","ProductionYear":2005,"Genres":["Drama"]
}`)
if got := Why(profile, candidate, 1); got[0] != "Because you've been watching 2000s dramas" {
t.Fatalf("viewing-era reason = %v", got)
}
}
func TestWhyCanNameAFavouriteTitle(t *testing.T) {
favourite := explainItem(t, `{
"Id":"h1","Name":"The Wire","Type":"Series","Genres":["Crime","Drama"]
}`)
candidate := explainItem(t, `{
"Id":"c1","Name":"We Own This City","Type":"Series","Genres":["Crime","Drama"]
}`)
if got := Why(BuildProfile(nil, []Item{favourite}), candidate, 1); got[0] != "Because you like The Wire" {
t.Fatalf("favourite-title reason = %v", got)
}
}
2026-08-02 22:10:19 +12:00
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
}