feat(playlists): For You composition v2 — multi-seed blend + weighted fresh tail
Two approved composition changes (#1269), mechanism only — the taste/fresh share stays data-decided (#1252) and pick_kind attribution is unchanged. Multi-seed blending: each day's build now seeds from up to 3 of the user's top-5 tracks (pickDailySeeds, the generalized daily shuffle) instead of one rotating anchor, so the mix spans neighborhoods within a day and stops feeling bipolar as the rotation swings between dissimilar seeds. Per-seed pools merge first-seen-deduped; the head is filled best-first under 50/30/20 per-seed quotas (60/40 for two seeds) so one neighborhood can't monopolize it, with thin-seed quota spilling best-first. Score-weighted fresh tail: the tail sample (rank 2*headN onward) was uniform — the 380th-best candidate as likely as the 101st. It now uses deterministic Efraimidis-Spirakis keys with weight halving every 50 ranks, so freshness keeps its "you'll probably enjoy this" half while still rotating daily. The retired single-seed picker's one other caller, You-might-like, moves to pickDailySeeds(n=1) — a single neighborhood per day is right for a short shelf, and the behavior note is inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
@@ -96,60 +96,65 @@ func TestUserIDHash_DifferentUserChangesHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// pickForYouSeedForDay rotates the chosen For-You seed across the
|
||||
// user's top-played candidates using userIDHash. Verifies the picker
|
||||
// is deterministic within a day, varies across days, and degrades
|
||||
// gracefully when fewer than 5 candidates exist.
|
||||
// pickDailySeeds shuffles the candidate pool daily-deterministically
|
||||
// and takes up to n — For-You uses n=forYouSeedCount for its
|
||||
// multi-seed blend (#1269), Songs-like uses n=3 via the
|
||||
// pickSeedArtistsForDay wrapper. Verifies determinism within a day,
|
||||
// variation across days, and graceful degradation on small pools.
|
||||
|
||||
func TestPickForYouSeedForDay_DeterministicWithinDay(t *testing.T) {
|
||||
func TestPickDailySeeds_DeterministicWithinDay(t *testing.T) {
|
||||
u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
|
||||
seeds := []pgtype.UUID{
|
||||
pool := []pgtype.UUID{
|
||||
{Bytes: [16]byte{10}, Valid: true},
|
||||
{Bytes: [16]byte{20}, Valid: true},
|
||||
{Bytes: [16]byte{30}, Valid: true},
|
||||
{Bytes: [16]byte{40}, Valid: true},
|
||||
{Bytes: [16]byte{50}, Valid: true},
|
||||
}
|
||||
a := pickForYouSeedForDay(seeds, u, "2026-05-04")
|
||||
b := pickForYouSeedForDay(seeds, u, "2026-05-04")
|
||||
if a != b {
|
||||
t.Fatalf("same day should pick same seed; got %v then %v", a, b)
|
||||
a := pickDailySeeds(pool, u, "2026-05-04", 3)
|
||||
b := pickDailySeeds(pool, u, "2026-05-04", 3)
|
||||
if len(a) != 3 || len(b) != 3 {
|
||||
t.Fatalf("lens = %d, %d; want 3, 3", len(a), len(b))
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
t.Fatalf("same day should pick same seeds; got %v then %v", a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickForYouSeedForDay_VariesAcrossDays(t *testing.T) {
|
||||
func TestPickDailySeeds_VariesAcrossDays(t *testing.T) {
|
||||
u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
|
||||
seeds := []pgtype.UUID{
|
||||
pool := []pgtype.UUID{
|
||||
{Bytes: [16]byte{10}, Valid: true},
|
||||
{Bytes: [16]byte{20}, Valid: true},
|
||||
{Bytes: [16]byte{30}, Valid: true},
|
||||
{Bytes: [16]byte{40}, Valid: true},
|
||||
{Bytes: [16]byte{50}, Valid: true},
|
||||
}
|
||||
picks := map[[16]byte]bool{}
|
||||
firstPicks := map[[16]byte]bool{}
|
||||
for i := 1; i <= 30; i++ {
|
||||
date := "2026-05-" + twoDigits(i)
|
||||
picks[pickForYouSeedForDay(seeds, u, date).Bytes] = true
|
||||
firstPicks[pickDailySeeds(pool, u, date, 3)[0].Bytes] = true
|
||||
}
|
||||
if len(picks) < 2 {
|
||||
t.Errorf("expected >=2 distinct seeds across 30 days; got %d", len(picks))
|
||||
if len(firstPicks) < 2 {
|
||||
t.Errorf("expected >=2 distinct lead seeds across 30 days; got %d", len(firstPicks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickForYouSeedForDay_SingleCandidate(t *testing.T) {
|
||||
func TestPickDailySeeds_SmallPool(t *testing.T) {
|
||||
u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
|
||||
only := pgtype.UUID{Bytes: [16]byte{99}, Valid: true}
|
||||
got := pickForYouSeedForDay([]pgtype.UUID{only}, u, "2026-05-04")
|
||||
if got != only {
|
||||
t.Errorf("single-seed pool should return that seed; got %v", got)
|
||||
got := pickDailySeeds([]pgtype.UUID{only}, u, "2026-05-04", 3)
|
||||
if len(got) != 1 || got[0] != only {
|
||||
t.Errorf("single-entry pool should return just that entry; got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickForYouSeedForDay_EmptyPool(t *testing.T) {
|
||||
func TestPickDailySeeds_EmptyPool(t *testing.T) {
|
||||
u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
|
||||
got := pickForYouSeedForDay(nil, u, "2026-05-04")
|
||||
if got.Valid {
|
||||
t.Errorf("empty pool should return zero UUID; got %v", got)
|
||||
if got := pickDailySeeds(nil, u, "2026-05-04", 3); len(got) != 0 {
|
||||
t.Errorf("empty pool should return nothing; got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user