package recommendation import ( "context" "testing" "time" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) func insertPlayEventDevice( t *testing.T, pool *pgxpool.Pool, userID, trackID pgtype.UUID, startedAt time.Time, device string, ) { t.Helper() ctx := context.Background() var sessionID pgtype.UUID if err := pool.QueryRow(ctx, `INSERT INTO play_sessions (user_id, started_at, last_event_at, client_id) VALUES ($1, $2, $2, 'ctx-dev-test') RETURNING id`, userID, startedAt).Scan(&sessionID); err != nil { t.Fatalf("insert play_session: %v", err) } if _, err := pool.Exec(ctx, `INSERT INTO play_events (user_id, track_id, session_id, started_at, was_skipped, device_class) VALUES ($1, $2, $3, $4, false, $5)`, userID, trackID, sessionID, startedAt, device); err != nil { t.Fatalf("insert play_event: %v", err) } } // TestLoadContextAffinity_DeviceNarrowing seeds one artist played only on // mobile and another only on web (all in the current time cell), and verifies // the current-device parameter narrows the affinity cell (#1551): on 'mobile' // the mobile artist out-scores the web artist and vice-versa, while the // device-agnostic (”) pass treats them equally. func TestLoadContextAffinity_DeviceNarrowing(t *testing.T) { pool := newPool(t) ctx := context.Background() q := dbq.New(pool) u := seedUser(t, pool, "ctx-dev") aMobile := seedArtist(t, pool, "MobileArtist", "") aWeb := seedArtist(t, pool, "WebArtist", "") alM := seedAlbumForArtist(t, pool, aMobile.ID, "AlbM") tM := seedTrackOnAlbum(t, pool, alM.ID, aMobile.ID, "TrkM") alW := seedAlbumForArtist(t, pool, aWeb.ID, "AlbW") tW := seedTrackOnAlbum(t, pool, alW.ID, aWeb.ID, "TrkW") now := time.Now() for i := 0; i < 5; i++ { insertPlayEventDevice(t, pool, u.ID, tM.ID, now, "mobile") insertPlayEventDevice(t, pool, u.ID, tW.ID, now, "web") } mob, err := LoadContextAffinity(ctx, q, u.ID, "mobile") if err != nil { t.Fatalf("mobile: %v", err) } if mob.Affinity(aMobile.ID) <= mob.Affinity(aWeb.ID) { t.Errorf("on mobile, mobile artist (%.3f) should out-score web artist (%.3f)", mob.Affinity(aMobile.ID), mob.Affinity(aWeb.ID)) } web, err := LoadContextAffinity(ctx, q, u.ID, "web") if err != nil { t.Fatalf("web: %v", err) } if web.Affinity(aWeb.ID) <= web.Affinity(aMobile.ID) { t.Errorf("on web, web artist (%.3f) should out-score mobile artist (%.3f)", web.Affinity(aWeb.ID), web.Affinity(aMobile.ID)) } // Device-agnostic ('' ): all plays fall in the same time cell, so the two // artists are treated identically — the device dimension is what separates // them above. agn, err := LoadContextAffinity(ctx, q, u.ID, "") if err != nil { t.Fatalf("agnostic: %v", err) } if agn.Affinity(aMobile.ID) != agn.Affinity(aWeb.ID) { t.Errorf("device-agnostic affinities should match, got mobile=%.3f web=%.3f", agn.Affinity(aMobile.ID), agn.Affinity(aWeb.ID)) } }