The same rewrite as d191a282, for four packages whose comments carried the
most history: tuning stories, quotes from the operator, single-install
measurements in library_scale.go, and 'used to' accounts of bugs. The
constraint each story protected stays as a present-tense reason (why tier 3
is reduced not removed, why the randomised arms take an order seed, why the
seed artist is eligible in Songs-like). Comments naming system_cron.go and
config.RecommendationConfig, neither of which holds what they claimed, now
name the scheduler and internal/recsettings.
Checked: non-comment tokens identical to HEAD; gofmt clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
2.9 KiB
Go
88 lines
2.9 KiB
Go
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: 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))
|
|
}
|
|
}
|