From c9824415920127d7fad2a34422fb8f9de178894c Mon Sep 17 00:00:00 2001 From: hermes Date: Wed, 30 Sep 2026 21:59:31 +0200 Subject: [PATCH] add internal/domain/playlist_test.go --- internal/domain/playlist_test.go | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 internal/domain/playlist_test.go diff --git a/internal/domain/playlist_test.go b/internal/domain/playlist_test.go new file mode 100644 index 0000000..e39c59f --- /dev/null +++ b/internal/domain/playlist_test.go @@ -0,0 +1,117 @@ +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)) +}