package recommendation import ( "math/rand" "testing" "time" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) func cand(id string, in ScoringInputs) Candidate { t := dbq.Track{Title: id} _ = t.ID.Scan("00000000-0000-0000-0000-" + id) // 12-char id padded return Candidate{Track: t, Inputs: in} } func TestShuffle_LikedRanksAboveUnliked(t *testing.T) { cs := []Candidate{ cand("000000000001", ScoringInputs{IsGeneralLiked: false}), cand("000000000002", ScoringInputs{IsGeneralLiked: true}), } out := Shuffle(cs, defaultWeights(), time.Now(), fixedRNG(0.5), 10) if out[0].Track.Title != "000000000002" { t.Errorf("liked track did not rank first: %+v", out) } } func TestShuffle_HighSkipRanksLast(t *testing.T) { cs := []Candidate{ cand("000000000001", ScoringInputs{PlayCount: 10, SkipCount: 10}), // ratio 1.0 cand("000000000002", ScoringInputs{PlayCount: 10, SkipCount: 0}), // ratio 0 cand("000000000003", ScoringInputs{PlayCount: 10, SkipCount: 5}), // ratio 0.5 } out := Shuffle(cs, defaultWeights(), time.Now(), fixedRNG(0.5), 10) if out[0].Track.Title != "000000000002" || out[2].Track.Title != "000000000001" { t.Errorf("skip-ratio ordering broken: %v", titles(out)) } } func TestShuffle_LimitTruncates(t *testing.T) { cs := make([]Candidate, 100) for i := range cs { cs[i] = cand("00000000000"+string(rune('a'+i%26)), ScoringInputs{}) } out := Shuffle(cs, defaultWeights(), time.Now(), fixedRNG(0.5), 10) if len(out) != 10 { t.Errorf("len = %d, want 10", len(out)) } } func TestShuffle_JitterDoesNotFlipStructuralWinner(t *testing.T) { // Liked vs unliked: even with many random RNG seeds, liked NEVER ranks below unliked. r := rand.New(rand.NewSource(42)) for i := 0; i < 500; i++ { cs := []Candidate{ cand("000000000001", ScoringInputs{IsGeneralLiked: false}), cand("000000000002", ScoringInputs{IsGeneralLiked: true}), } out := Shuffle(cs, defaultWeights(), time.Now(), r.Float64, 10) if out[0].Track.Title != "000000000002" { t.Fatalf("iter %d: liked did not rank first; out=%v", i, titles(out)) } } } func TestShuffle_Empty_ReturnsEmpty(t *testing.T) { out := Shuffle(nil, defaultWeights(), time.Now(), fixedRNG(0.5), 10) if len(out) != 0 { t.Errorf("len = %d, want 0", len(out)) } } func titles(cs []Candidate) []string { out := make([]string, 0, len(cs)) for _, c := range cs { out = append(out, c.Track.Title) } return out } // pgtype-uuid placeholder: Candidate doesn't need a real UUID for these // pure tests; the Track.Title field is the human-readable handle. Track ID // is left as zero pgtype.UUID and never compared. var _ pgtype.UUID