diff --git a/internal/anime/recommendations/recommendations_test.go b/internal/anime/recommendations/recommendations_test.go index 2e3381ba..7017c528 100644 --- a/internal/anime/recommendations/recommendations_test.go +++ b/internal/anime/recommendations/recommendations_test.go @@ -74,6 +74,47 @@ func TestScoreRecommendationCandidateRewardsProfileOverlap(t *testing.T) { } } +func TestScoreRecommendationCandidateBuildsRationaleFromProfileMatches(t *testing.T) { + now := time.Date(2026, time.June, 4, 12, 0, 0, 0, time.UTC) + profile := userTasteProfile{ + genres: map[int]float64{1: 2.0}, + themes: map[int]float64{10: 1.5}, + studios: map[int]float64{20: 1.0}, + demographics: map[int]float64{30: 1.0}, + } + + candidate := scoreRecommendationCandidate(now, profile, jikan.Anime{ + MalID: 10, + Genres: []jikan.NamedEntity{{MalID: 1, Name: "Action"}}, + Themes: []jikan.NamedEntity{{MalID: 10, Name: "School"}}, + Studios: []jikan.NamedEntity{{MalID: 20, Name: "Production I.G"}}, + Demographics: []jikan.NamedEntity{{MalID: 30, Name: "Shounen"}}, + }, 5.0, 0) + + want := []string{"Action", "School", "Production I.G", "Shounen"} + if !slices.Equal(candidate.rationale, want) { + t.Fatalf("expected profile match rationale %v, got %v", want, candidate.rationale) + } +} + +func TestScoreRecommendationCandidateOmitsRationaleWhenSignalsAreWeak(t *testing.T) { + now := time.Date(2026, time.June, 4, 12, 0, 0, 0, time.UTC) + profile := userTasteProfile{ + genres: map[int]float64{1: 2.0}, + themes: map[int]float64{}, + studios: map[int]float64{}, + demographics: map[int]float64{}, + } + + candidate := scoreRecommendationCandidate(now, profile, jikan.Anime{ + MalID: 10, + }, 5.0, 0) + + if len(candidate.rationale) != 0 { + t.Fatalf("expected no rationale for weak signals, got %v", candidate.rationale) + } +} + func TestBuildTasteProfileUsesSeedWeights(t *testing.T) { now := time.Date(2026, time.June, 4, 12, 0, 0, 0, time.UTC) diff --git a/templates/renderer_test.go b/templates/renderer_test.go index 726d86f3..0b08432c 100644 --- a/templates/renderer_test.go +++ b/templates/renderer_test.go @@ -107,6 +107,37 @@ func TestRenderWithNonStringFragment(t *testing.T) { } } +func TestTopPicksTemplateRendersRecommendationRationale(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = r.ExecuteFragment(&buf, "top_picks.gohtml", "content", map[string]any{ + "Animes": []domain.Anime{ + { + Anime: jikan.Anime{ + MalID: 1, + Title: "Haikyuu!!", + }, + RecommendationRationale: []string{"Sports", "Production I.G"}, + }, + }, + "WatchlistMap": map[int64]bool{}, + }) + if err != nil { + t.Fatalf("ExecuteFragment error: %v", err) + } + + body := buf.String() + for _, want := range []string{"Why this was picked", "Sports", "Production I.G"} { + if !strings.Contains(body, want) { + t.Fatalf("top picks template missing %q in output:\n%s", want, body) + } + } +} + func TestWatchTemplateEscapesJSONDataAttributes(t *testing.T) { r, err := ProvideRenderer() if err != nil {