test: add recommendation rationale coverage
This commit is contained in:
@@ -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) {
|
func TestBuildTasteProfileUsesSeedWeights(t *testing.T) {
|
||||||
now := time.Date(2026, time.June, 4, 12, 0, 0, 0, time.UTC)
|
now := time.Date(2026, time.June, 4, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
func TestWatchTemplateEscapesJSONDataAttributes(t *testing.T) {
|
||||||
r, err := ProvideRenderer()
|
r, err := ProvideRenderer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user