package playlists import ( "testing" "github.com/jackc/pgx/v5/pgtype" ) func mixTrack(b byte, kind string) discoverTrack { return discoverTrack{ID: pgtype.UUID{Bytes: [16]byte{b}, Valid: true}, PickKind: kind} } func idsOf(pool []discoverTrack) []byte { out := make([]byte, len(pool)) for i, t := range pool { out[i] = t.ID.Bytes[0] } return out } func TestRotateForDay_RotatesWithinTierBlocks(t *testing.T) { // Tiered pools arrive tier-ordered; rotation must vary the daily // slice WITHIN each tier without hoisting tier-3 filler above // tier-1's exact fits (#1267). pool := []discoverTrack{ mixTrack(1, pickKindTier1), mixTrack(2, pickKindTier1), mixTrack(3, pickKindTier1), mixTrack(4, pickKindTier2), mixTrack(5, pickKindTier2), mixTrack(6, pickKindTier3), } u := pgtype.UUID{Bytes: [16]byte{42}, Valid: true} got := rotateForDay(pool, u, "2026-07-03") if len(got) != len(pool) { t.Fatalf("len = %d, want %d", len(got), len(pool)) } wantKinds := []string{ pickKindTier1, pickKindTier1, pickKindTier1, pickKindTier2, pickKindTier2, pickKindTier3, } for i, k := range wantKinds { if got[i].PickKind != k { t.Fatalf("pos %d kind = %q, want %q (tier order broken: %v)", i, got[i].PickKind, k, idsOf(got)) } } // Each block is a rotation of its input: contiguity check for the // 3-element tier-1 block (successor relation preserved cyclically). wantOrder := map[byte]byte{1: 2, 2: 3, 3: 1} for i := 0; i < 2; i++ { if got[i+1].ID.Bytes[0] != wantOrder[got[i].ID.Bytes[0]] { t.Errorf("tier1 block is not a rotation: %v", idsOf(got[:3])) break } } // Determinism within a day. again := rotateForDay(pool, u, "2026-07-03") for i := range got { if got[i].ID != again[i].ID { t.Fatal("rotation must be deterministic for the same (user, day)") } } } func TestRotateForDay_UntieredPoolIsWholeRotation(t *testing.T) { // Untiered pools (all PickKind "") are one block — the original // whole-pool rotation semantics. pool := []discoverTrack{mixTrack(1, ""), mixTrack(2, ""), mixTrack(3, ""), mixTrack(4, "")} got := rotateForDay(pool, pgtype.UUID{Bytes: [16]byte{7}, Valid: true}, "2026-07-03") if len(got) != 4 { t.Fatalf("len = %d, want 4", len(got)) } wantNext := map[byte]byte{1: 2, 2: 3, 3: 4, 4: 1} for i := 0; i < 3; i++ { cur, next := got[i].ID.Bytes[0], got[i+1].ID.Bytes[0] if wantNext[cur] != next { t.Errorf("not a rotation of the input: %v", idsOf(got)) break } } } func TestFinishMix_PropagatesPickKind(t *testing.T) { // Tier stamps ride discoverTrack through diversify/truncate into // the rankedCandidates that insertSystemPlaylist persists (#1267). pool := []discoverTrack{mixTrack(1, pickKindTier1), mixTrack(2, pickKindTier2)} got := finishMix(pool, false) if len(got) != 2 { t.Fatalf("len = %d, want 2", len(got)) } if got[0].PickKind != pickKindTier1 || got[1].PickKind != pickKindTier2 { t.Errorf("pick kinds = %q, %q — want tier1, tier2", got[0].PickKind, got[1].PickKind) } } func TestPickKindForMixTier(t *testing.T) { cases := []struct { tier int32 want string }{{1, pickKindTier1}, {2, pickKindTier2}, {3, pickKindTier3}, {9, pickKindTier3}} for _, c := range cases { if got := pickKindForMixTier(c.tier); got != c.want { t.Errorf("pickKindForMixTier(%d) = %q, want %q", c.tier, got, c.want) } } }