package domain import ( "testing" "time" ) func mkTrack(id string, created time.Time, rating int) Track { return Track{ ID: id, Genre: "synthwave", Title: "Song " + id, Style: "dreamy synth", Source: "generated", CreatedAt: UnixTime{created}, Rating: rating, } } func TestExcludesDisliked(t *testing.T) { now := time.Now() tracks := []Track{ mkTrack("a", now.Add(-3*time.Hour), 0), mkTrack("b", now.Add(-2*time.Hour), -1), // disliked mkTrack("c", now.Add(-1*time.Hour), 1), } got := FilterPlayable(tracks) if len(got) != 2 { t.Fatalf("want 2 playable, got %d", len(got)) } for _, tr := range got { if tr.ID == "b" { t.Fatal("disliked track b must be excluded") } } } func TestRollingPlaylistSizeAndNoDupes(t *testing.T) { now := time.Now() var tracks []Track for i := 0; i < 20; i++ { tracks = append(tracks, mkTrack(string(rune('a'+i)), now.Add(time.Duration(i)*time.Minute), 0)) } pl := BuildPlaylist(tracks, 8, 3, time.Now().Unix()) if len(pl) != 8 { t.Fatalf("want 8, got %d", len(pl)) } seen := map[string]bool{} for _, tr := range pl { if seen[tr.ID] { t.Fatalf("duplicate %s in playlist", tr.ID) } seen[tr.ID] = true } } func TestNewnessBiasPrefersNewest(t *testing.T) { now := time.Now() var tracks []Track for i := 0; i < 50; i++ { tracks = append(tracks, mkTrack(idFor(i), now.Add(-time.Duration(50-i)*time.Minute), 0)) } // strong bias: bias=1 means newest rank0 weight=1, rank49 weight=1/50 newestCount := 0 const runs = 200 for r := 0; r < runs; r++ { pl := BuildPlaylist(tracks, 5, 1, int64(r*7919)) for _, tr := range pl { if tr.ID == idFor(49) { // the newest newestCount++ } } } // with strong bias the newest track should appear in most playlists if newestCount < runs/2 { t.Fatalf("newest track appeared in only %d/%d playlists — bias not working", newestCount, runs) } } func TestUniformWhenBiasHigh(t *testing.T) { // bias >> pool size => near-uniform; just sanity check no panic and full size now := time.Now() var tracks []Track for i := 0; i < 10; i++ { tracks = append(tracks, mkTrack(idFor(i), now.Add(-time.Duration(10-i)*time.Minute), 0)) } pl := BuildPlaylist(tracks, 10, 1000, 42) if len(pl) != 10 { t.Fatalf("want 10, got %d", len(pl)) } } func TestEmptyAndSmallPool(t *testing.T) { if pl := BuildPlaylist(nil, 8, 3, 1); len(pl) != 0 { t.Fatal("empty pool must give empty playlist") } now := time.Now() pl := BuildPlaylist([]Track{mkTrack("x", now, 0)}, 8, 3, 1) if len(pl) != 1 { t.Fatalf("pool of 1 should give playlist of 1, got %d", len(pl)) } } func TestTitleForDisplay(t *testing.T) { tr := mkTrack("t1", time.Now(), 0) if got := tr.DisplayTitle(); got != "Song t1" { t.Fatalf("DisplayTitle = %q", got) } tr.Title = "" if got := tr.DisplayTitle(); got != tr.Style { t.Fatalf("fallback DisplayTitle = %q, want style %q", got, tr.Style) } } func idFor(i int) string { return "id" + string(rune('A'+i%26)) + string(rune('a'+i/26)) }