first commit

This commit is contained in:
Tomas Dvorak
2026-04-13 17:46:58 +02:00
commit 6e8fedf534
234 changed files with 53808 additions and 0 deletions
@@ -0,0 +1,161 @@
package recommendation
import (
"context"
"testing"
"time"
)
type testProvider struct {
snapshot CatalogSnapshot
}
func (p testProvider) Snapshot(context.Context, string) (CatalogSnapshot, error) {
return p.snapshot, nil
}
func TestRecommendBlendsContentAndCollaborativeSignals(t *testing.T) {
now := time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC)
engine := NewEngine(EngineConfig{
Now: func() time.Time { return now },
ContentWeight: 0.44,
CollabWeight: 0.28,
PopularityWeight: 0.08,
ExplorationWeight: 0.20,
DiversityLambda: 0.74,
})
tracks := []Track{
track("liked", "Known Good", "A", []string{"synth"}, 0.7, AudioFeatures{Danceability: 0.8, Energy: 0.8, Loudness: -5, Valence: 0.7, Tempo: 120, TimeSignature: 4, Key: 1, Mode: 1}),
track("neighbor", "Neighbor Pick", "B", []string{"synth"}, 0.6, AudioFeatures{Danceability: 0.76, Energy: 0.77, Loudness: -6, Valence: 0.66, Tempo: 121, TimeSignature: 4, Key: 2, Mode: 1}),
track("far", "Far Away", "C", []string{"folk"}, 0.5, AudioFeatures{Danceability: 0.2, Energy: 0.2, Loudness: -18, Acousticness: 0.9, Valence: 0.3, Tempo: 80, TimeSignature: 3, Key: 9, Mode: 0}),
}
recs, profile, err := engine.Recommend(context.Background(), testProvider{snapshot: CatalogSnapshot{
Tracks: tracks,
Interactions: []Interaction{
{UserID: "u1", TrackID: "liked", Type: InteractionLike, OccurredAt: now.Add(-time.Hour)},
{UserID: "u1", TrackID: "far", Type: InteractionSkip, OccurredAt: now.Add(-2 * time.Hour)},
{UserID: "n1", TrackID: "liked", Type: InteractionLike, OccurredAt: now.Add(-3 * time.Hour)},
{UserID: "n1", TrackID: "far", Type: InteractionSkip, OccurredAt: now.Add(-4 * time.Hour)},
{UserID: "n1", TrackID: "neighbor", Type: InteractionLike, OccurredAt: now.Add(-5 * time.Hour)},
},
Controls: UserControls{UserID: "u1", AllowExplicit: true},
}}, RecommendRequest{UserID: "u1", Limit: 2})
if err != nil {
t.Fatalf("recommend: %v", err)
}
if len(recs) == 0 {
t.Fatal("expected recommendations")
}
if recs[0].Track.ID != "neighbor" {
t.Fatalf("expected neighbor track first, got %q", recs[0].Track.ID)
}
if profile.Confidence <= 0 {
t.Fatalf("expected non-zero confidence, got %v", profile.Confidence)
}
}
func TestRecommendRespectsControls(t *testing.T) {
now := time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC)
engine := NewEngine(EngineConfig{Now: func() time.Time { return now }, ContentWeight: 1, DiversityLambda: 0.8})
explicit := track("explicit", "Explicit", "A", []string{"pop"}, 0.5, AudioFeatures{Danceability: 0.7, Energy: 0.7, Loudness: -6, Valence: 0.7, Tempo: 120, TimeSignature: 4})
explicit.Explicit = true
clean := track("clean", "Clean", "A", []string{"pop"}, 0.5, AudioFeatures{Danceability: 0.69, Energy: 0.71, Loudness: -6, Valence: 0.68, Tempo: 121, TimeSignature: 4})
recs, _, err := engine.Recommend(context.Background(), testProvider{snapshot: CatalogSnapshot{
Tracks: []Track{
track("seed", "Seed", "A", []string{"pop"}, 0.5, AudioFeatures{Danceability: 0.7, Energy: 0.7, Loudness: -6, Valence: 0.7, Tempo: 120, TimeSignature: 4}),
explicit,
clean,
},
Interactions: []Interaction{{UserID: "u1", TrackID: "seed", Type: InteractionLike, OccurredAt: now}},
Controls: UserControls{UserID: "u1", AllowExplicit: false},
}}, RecommendRequest{UserID: "u1", Limit: 10})
if err != nil {
t.Fatalf("recommend: %v", err)
}
for _, rec := range recs {
if rec.Track.ID == "explicit" {
t.Fatal("explicit track should be filtered")
}
}
if len(recs) != 1 || recs[0].Track.ID != "clean" {
t.Fatalf("expected only clean track, got %#v", recs)
}
}
func TestRecommendUsesMetadataWhenAudioFeaturesAreMissing(t *testing.T) {
now := time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC)
engine := NewEngine(EngineConfig{Now: func() time.Time { return now }, ContentWeight: 1, DiversityLambda: 0.85})
recs, _, err := engine.Recommend(context.Background(), testProvider{snapshot: CatalogSnapshot{
Tracks: []Track{
track("seed", "Seed", "Seed Artist", []string{"synthpop"}, 0.5, AudioFeatures{}),
track("genre-match", "Genre Match", "Other Artist", []string{"synthpop"}, 0.5, AudioFeatures{}),
track("unrelated", "Unrelated", "Far Artist", []string{"folk"}, 0.5, AudioFeatures{}),
},
Controls: UserControls{UserID: "u1", AllowExplicit: true},
}}, RecommendRequest{UserID: "u1", SeedTrackIDs: []string{"seed"}, Limit: 2})
if err != nil {
t.Fatalf("recommend: %v", err)
}
if len(recs) == 0 {
t.Fatal("expected recommendations")
}
if recs[0].Track.ID != "genre-match" {
t.Fatalf("expected metadata genre match first, got %q", recs[0].Track.ID)
}
for _, rec := range recs {
if rec.Track.ID == "seed" {
t.Fatal("seed track should not be recommended back")
}
}
}
func TestRecommendPenalizesSkippedNeighborhoods(t *testing.T) {
now := time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC)
engine := NewEngine(EngineConfig{
Now: func() time.Time { return now },
ContentWeight: 0.74,
PopularityWeight: 0.08,
ExplorationWeight: 0.18,
DiversityLambda: 0.9,
})
audio := AudioFeatures{Danceability: 0.74, Energy: 0.76, Loudness: -5, Speechiness: 0.05, Acousticness: 0.12, Instrumentalness: 0.04, Liveness: 0.12, Valence: 0.66, Tempo: 124, TimeSignature: 4, Key: 2, Mode: 1}
recs, _, err := engine.Recommend(context.Background(), testProvider{snapshot: CatalogSnapshot{
Tracks: []Track{
track("liked", "Liked", "A", []string{"dance"}, 0.7, audio),
track("skipped", "Skipped", "B", []string{"metal"}, 0.7, audio),
track("penalized", "Penalized", "C", []string{"metal"}, 0.7, audio),
track("safe", "Safe", "D", []string{"dance"}, 0.62, AudioFeatures{Danceability: 0.72, Energy: 0.74, Loudness: -6, Speechiness: 0.05, Acousticness: 0.14, Instrumentalness: 0.05, Liveness: 0.1, Valence: 0.64, Tempo: 125, TimeSignature: 4, Key: 3, Mode: 1}),
},
Interactions: []Interaction{
{UserID: "u1", TrackID: "liked", Type: InteractionLike, OccurredAt: now.Add(-time.Hour)},
{UserID: "u1", TrackID: "skipped", Type: InteractionSkip, OccurredAt: now.Add(-30 * time.Minute)},
},
Controls: UserControls{UserID: "u1", AllowExplicit: true},
}}, RecommendRequest{UserID: "u1", Limit: 2})
if err != nil {
t.Fatalf("recommend: %v", err)
}
if len(recs) == 0 {
t.Fatal("expected recommendations")
}
if recs[0].Track.ID != "safe" {
t.Fatalf("expected non-skipped neighborhood first, got %q", recs[0].Track.ID)
}
}
func track(id, title, artist string, genres []string, popularity float64, features AudioFeatures) Track {
return Track{
ID: id,
Title: title,
Artist: artist,
Genres: genres,
Popularity: popularity,
Features: features,
DiscoveryAllowed: true,
}
}