package recommendation import "testing" // Songs-like's pool must lean on arms that MEASURE distance from the seed. // The default gave ~29% of candidates a sim_score of literally 0 // (taste_overlap and random_fill are both `0.0::float8` in // recommendation.sql), which is what let "Songs like X" wander. func TestSongsLikeLimits_FavourTheArmsThatMeasureTheSeed(t *testing.T) { d := DefaultCandidateSourceLimits() s := SongsLikeCandidateSourceLimits() if s.LBSimilar <= d.LBSimilar { t.Errorf("LBSimilar %d is not above the default %d — the only arm that "+ "measures track-level distance from the seed should be favoured here", s.LBSimilar, d.LBSimilar) } // The two seed-INDEPENDENT arms, which is the whole complaint. zeroSimDefault := d.TasteOverlap + d.RandomFill zeroSimSongsLike := s.TasteOverlap + s.RandomFill if zeroSimSongsLike >= zeroSimDefault { t.Errorf("seed-independent arms total %d, not reduced from the default %d; "+ "these carry sim_score 0 by construction", zeroSimSongsLike, zeroSimDefault) } } // RULE 131: a system playlist degrades, it never vanishes. // // Zeroing the seed-independent arms was the first instinct and is exactly the // vanish-or-nothing shape that rule forbids: a seed whose artist has thin // ListenBrainz coverage would yield a short mix or none at all. They are the // tier-3 FLOOR — reduced hard, never removed — and the songs_like weights are // what keep them at the bottom of the ranking rather than out of the pool. // // "A few tracks further from the seed than we would like" beats "no playlist". func TestSongsLikeLimits_KeepATierThreeFloor(t *testing.T) { s := SongsLikeCandidateSourceLimits() if s.RandomFill <= 0 { t.Error("RandomFill is zero: a seed with thin similarity coverage now produces " + "a short or empty mix instead of degrading (rule 131)") } if s.TasteOverlap <= 0 { t.Error("TasteOverlap is zero: the graded floor is gone, leaving only random " + "fill between a sparse seed and an empty playlist (rule 131)") } } // The pool should stay roughly the size it was — this change is about // COMPOSITION, not about starving the surface. A much smaller pool would also // shrink what the per-artist cap has to work with. func TestSongsLikeLimits_KeepThePoolRoughlyTheSameSize(t *testing.T) { total := func(l CandidateSourceLimits) int { return l.LBSimilar + l.SimilarArtist + l.TagOverlap + l.LikesOverlap + l.RandomFill + l.TasteOverlap + l.UserCoplay } d, s := total(DefaultCandidateSourceLimits()), total(SongsLikeCandidateSourceLimits()) if s < d/2 { t.Errorf("songs_like pool is %d against the default %d — less than half; "+ "this was meant to re-weight the pool, not starve it", s, d) } } // The constraint that is invisible in the numbers, and that this file exists // to keep visible. // // `likes_overlap` and `random_fill` end in a bare `ORDER BY random()` with no // daily seed (recommendation.sql:118, :161). Such an arm returns a stable set // only while its LIMIT exceeds the eligible rows; below that it returns a // random SUBSET that differs between two builds on the same day, and the // daily-determinism promise quietly stops holding. // // This is not hypothetical — it is how this change first failed CI. Cutting // RandomFill to 10 broke TestBuildSystemPlaylists_DailyNonceDeterminism, // whose library is smaller than the default limit and whose determinism was // therefore an accident of the limit exceeding the library. // // Growing these arms is always safe. Only shrinking is, and the fix that // would make shrinking safe is a seeded ordering (#3889), not a smaller // number here. func TestSongsLikeLimits_DoNotShrinkTheUnseededRandomArms(t *testing.T) { d := DefaultCandidateSourceLimits() s := SongsLikeCandidateSourceLimits() for _, tc := range []struct { arm string songsLike, dflt int }{ {"RandomFill", s.RandomFill, d.RandomFill}, {"LikesOverlap", s.LikesOverlap, d.LikesOverlap}, } { if tc.songsLike < tc.dflt { t.Errorf("%s cut from %d to %d. That arm is ordered by unseeded random(), "+ "so a smaller limit makes pool membership vary between same-day "+ "rebuilds — it breaks daily determinism rather than merely narrowing "+ "the mix. Fix the ordering (#3889) before trimming this.", tc.arm, tc.dflt, tc.songsLike) } } }