fix(taste): drop unused now param + tighten test (golangci-lint revive)
test-go / test (push) Successful in 28s
test-go / integration (push) Successful in 4m25s

golangci-lint v2 (CI-only; local is v1) flagged two unused-parameter issues:
- BuildTasteProfile's `now` was genuinely dead — decay/windowing are computed
  DB-side via now(), so no Go-side timestamp is threaded. Removed it (a
  phase-3 context model that needs a pinned reference time would re-add it);
  updated the scheduler call site.
- the degenerate-params engagement test ignored t; reworked it to assert the
  result stays in [-1,1], which also strengthens the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 20:58:43 -04:00
parent 6e0d0e5723
commit 13b3fca949
4 changed files with 17 additions and 11 deletions
+1 -1
View File
@@ -227,7 +227,7 @@ func (s *Scheduler) runStartupCatchUp(ctx context.Context, users []dbq.ListActiv
// build can read a fresh profile (phase-2 consumption); both steps are // build can read a fresh profile (phase-2 consumption); both steps are
// best-effort and a failure in one is logged without blocking the other. // best-effort and a failure in one is logged without blocking the other.
func (s *Scheduler) rebuildUserDaily(ctx context.Context, userID pgtype.UUID, now time.Time) { func (s *Scheduler) rebuildUserDaily(ctx context.Context, userID pgtype.UUID, now time.Time) {
if err := taste.BuildTasteProfile(ctx, s.pool, s.logger, userID, now, taste.DefaultConfig()); err != nil { if err := taste.BuildTasteProfile(ctx, s.pool, s.logger, userID, taste.DefaultConfig()); err != nil {
s.logger.Warn("scheduler: taste profile rebuild failed", s.logger.Warn("scheduler: taste profile rebuild failed",
"user_id", uuidStringPL(userID), "err", err) "user_id", uuidStringPL(userID), "err", err)
} }
+7 -3
View File
@@ -44,11 +44,15 @@ func TestEngagement_MonotonicNonDecreasing(t *testing.T) {
} }
} }
func TestEngagement_DegenerateParamsNoPanic(t *testing.T) { func TestEngagement_DegenerateParamsStayInRange(t *testing.T) {
// Non-increasing thresholds must collapse ramps, not divide by zero. // Non-increasing thresholds must collapse ramps (no divide-by-zero) and
// still produce a value in [-1, 1].
p := EngagementParams{HardSkip: 0.5, NeutralCompletion: 0.5, FullCompletion: 0.5} p := EngagementParams{HardSkip: 0.5, NeutralCompletion: 0.5, FullCompletion: 0.5}
for _, c := range []float64{0, 0.25, 0.5, 0.75, 1} { for _, c := range []float64{0, 0.25, 0.5, 0.75, 1} {
_ = Engagement(c, p) got := Engagement(c, p)
if got < -1 || got > 1 {
t.Errorf("Engagement(%.2f) = %.3f out of [-1,1]", c, got)
}
} }
} }
+5 -2
View File
@@ -11,7 +11,6 @@ import (
"log/slog" "log/slog"
"sort" "sort"
"strings" "strings"
"time"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
@@ -81,9 +80,13 @@ func DefaultConfig() Config {
// Self-contained transaction for the replace. Returns an error only on a // Self-contained transaction for the replace. Returns an error only on a
// fatal DB failure; the daily caller logs and proceeds to playlist building // fatal DB failure; the daily caller logs and proceeds to playlist building
// regardless (best-effort). // regardless (best-effort).
//
// The decay reference time is the database clock (age is computed in SQL via
// now()), so no Go-side timestamp is threaded here; a phase-3 context model
// that needs a pinned reference time would add one.
func BuildTasteProfile( func BuildTasteProfile(
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger,
userID pgtype.UUID, now time.Time, cfg Config, userID pgtype.UUID, cfg Config,
) error { ) error {
q := dbq.New(pool) q := dbq.New(pool)
+4 -5
View File
@@ -155,7 +155,7 @@ func TestBuildTasteProfile_PositiveVsNegative(t *testing.T) {
seedPlay(t, pool, u.ID, skippedTrack.ID, now.Add(-time.Duration(i+1)*time.Hour), 0.02) seedPlay(t, pool, u.ID, skippedTrack.ID, now.Add(-time.Duration(i+1)*time.Hour), 0.02)
} }
if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, now, DefaultConfig()); err != nil { if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, DefaultConfig()); err != nil {
t.Fatalf("build: %v", err) t.Fatalf("build: %v", err)
} }
@@ -190,7 +190,7 @@ func TestBuildTasteProfile_AggregationProtectsArtist(t *testing.T) {
skipTrack := seedTrack(t, pool, fav.ID, "Indie") skipTrack := seedTrack(t, pool, fav.ID, "Indie")
seedPlay(t, pool, u.ID, skipTrack.ID, now.Add(-2*time.Hour), 0.02) seedPlay(t, pool, u.ID, skipTrack.ID, now.Add(-2*time.Hour), 0.02)
if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, now, DefaultConfig()); err != nil { if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, DefaultConfig()); err != nil {
t.Fatalf("build: %v", err) t.Fatalf("build: %v", err)
} }
w, ok := artistWeight(t, pool, u.ID, fav.ID) w, ok := artistWeight(t, pool, u.ID, fav.ID)
@@ -203,7 +203,6 @@ func TestBuildTasteProfile_AggregationProtectsArtist(t *testing.T) {
// plays still earns a positive weight from the like bonus. // plays still earns a positive weight from the like bonus.
func TestBuildTasteProfile_ArtistLikeBonus(t *testing.T) { func TestBuildTasteProfile_ArtistLikeBonus(t *testing.T) {
pool := newPool(t) pool := newPool(t)
now := time.Now().UTC()
u := seedUser(t, pool, "tp3") u := seedUser(t, pool, "tp3")
liked := seedArtist(t, pool, "LikedOnly") liked := seedArtist(t, pool, "LikedOnly")
if _, err := pool.Exec(context.Background(), if _, err := pool.Exec(context.Background(),
@@ -212,7 +211,7 @@ func TestBuildTasteProfile_ArtistLikeBonus(t *testing.T) {
t.Fatalf("seed artist like: %v", err) t.Fatalf("seed artist like: %v", err)
} }
if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, now, DefaultConfig()); err != nil { if err := BuildTasteProfile(context.Background(), pool, discardLogger(), u.ID, DefaultConfig()); err != nil {
t.Fatalf("build: %v", err) t.Fatalf("build: %v", err)
} }
w, ok := artistWeight(t, pool, u.ID, liked.ID) w, ok := artistWeight(t, pool, u.ID, liked.ID)
@@ -240,7 +239,7 @@ func TestBuildTasteProfile_AtomicReplace(t *testing.T) {
return n return n
} }
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
if err := BuildTasteProfile(ctx, pool, discardLogger(), u.ID, now, DefaultConfig()); err != nil { if err := BuildTasteProfile(ctx, pool, discardLogger(), u.ID, DefaultConfig()); err != nil {
t.Fatalf("build %d: %v", i, err) t.Fatalf("build %d: %v", i, err)
} }
} }