fix(playlists): Songs-like mixes no longer vanish after a quiet week
PickSeedArtists had a hard 7-day window with no fallback: a week without listening emptied the seed pool, produceSeedMixes returned zero playlists, and the daily atomic-replace build deleted every existing "Songs like X" mix until the user played something again (#1255). The query now falls back through widening engagement windows — 7d → 30d → all-time → liked artists — the same tiered shape that fixed the identical vanish for For You's seeds (PickTopPlayedTracksForUser). Like-boost scoring is preserved in every tier. All returned rows share the winning tier, and produceSeedMixes maps it onto the rule-#131 pick-kind ladder (7d = tier1 exact, 30d = tier2, all-time/liked = tier3) and stamps the built tracks — the #1270 provenance pipeline then attributes plays and skips to seed freshness, so the metrics card can say whether stale-seeded mixes actually perform worse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
@@ -135,6 +135,59 @@ func TestBuildSystemPlaylists_SufficientActivity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemPlaylists_StaleActivity_SongsLikeSurvives(t *testing.T) {
|
||||
// #1255: the seed-artist query's old hard 7-day window emptied the
|
||||
// pool after a quiet week, and the atomic-replace build then deleted
|
||||
// every "Songs like X" mix. With the tiered fallback, plays that are
|
||||
// ~20 days old (outside 7d, inside 30d) must still seed the mixes —
|
||||
// and the built tracks carry the tier2 stamp so metrics can compare
|
||||
// stale-seeded mixes against fresh ones.
|
||||
pool := newPool(t)
|
||||
logger := discardLogger()
|
||||
u := seedUser(t, pool, "stale1")
|
||||
old := time.Now().UTC().Add(-20 * 24 * time.Hour)
|
||||
for a := 0; a < 4; a++ {
|
||||
for k := 0; k < 3; k++ {
|
||||
tk := seedTrack(t, pool,
|
||||
"stale1-track-"+string(rune('A'+a))+string(rune('0'+k)),
|
||||
"stale1-artist-"+string(rune('A'+a)))
|
||||
for p := 0; p < 3; p++ {
|
||||
seedPlayEvent(t, pool, u.ID, tk.ID,
|
||||
old.Add(-time.Duration(a*10+k+p)*time.Hour), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
|
||||
rows, err := pool.Query(context.Background(), `
|
||||
SELECT DISTINCT COALESCE(pt.pick_kind, '<null>')
|
||||
FROM playlist_tracks pt
|
||||
JOIN playlists p ON p.id = pt.playlist_id
|
||||
WHERE p.user_id = $1 AND p.system_variant = 'songs_like_artist'
|
||||
`, u.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("query pick_kinds: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var kinds []string
|
||||
for rows.Next() {
|
||||
var k string
|
||||
if err := rows.Scan(&k); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
kinds = append(kinds, k)
|
||||
}
|
||||
if len(kinds) == 0 {
|
||||
t.Fatal("expected songs_like_artist tracks from the 30-day fallback tier; got none (the vanish bug)")
|
||||
}
|
||||
if len(kinds) != 1 || kinds[0] != "tier2" {
|
||||
t.Errorf("pick_kinds = %v, want exactly [tier2] (30-day fallback seeds)", kinds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemPlaylists_QuarantineExcluded(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
logger := discardLogger()
|
||||
|
||||
Reference in New Issue
Block a user