Recommendation relevance, the rollback unit, and a version that names what shipped #131

Merged
bvandeusen merged 11 commits from dev into main 2026-09-10 23:37:57 -04:00
Showing only changes of commit f5dd4462de - Show all commits
+65 -1
View File
@@ -2,8 +2,10 @@ package playlists_test
import ( import (
"context" "context"
"fmt"
"io" "io"
"log/slog" "log/slog"
"path/filepath"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -287,6 +289,68 @@ func TestBuildSystemPlaylists_Concurrency(t *testing.T) {
} }
} }
// seedSharedArtistLibrary seeds artists that genuinely OWN several tracks.
//
// seedActiveLibrary cannot be used for this: its helper documents that
// "artist and album are not deduplicated across calls (mbid-less upsert)",
// so every track gets its own artist row and a seed artist always has
// exactly one track — the seed itself, which is excluded. A same-artist
// assertion against that fixture can never pass no matter what the code
// does, which is how the first version of this test failed.
//
// Albums are not deduplicated either, for the same mbid-less reason, so each
// track ends up under its own album row however the titles are written. That
// is convenient here rather than a problem: it means the per-ALBUM cap (2)
// never binds, and the per-ARTIST cap (3) is unambiguously the thing under
// test. Do not "fix" the album titles into something shared without checking
// which cap you are then measuring.
func seedSharedArtistLibrary(
t *testing.T, pool *pgxpool.Pool, name string, numArtists, tracksPerArtist int,
) (dbq.User, []pgtype.UUID) {
t.Helper()
q := dbq.New(pool)
ctx := context.Background()
u := seedUser(t, pool, name)
now := time.Now().UTC()
dir := t.TempDir()
artistIDs := make([]pgtype.UUID, 0, numArtists)
for a := 0; a < numArtists; a++ {
artistName := name + "-shared-" + string(rune('A'+a))
ar, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{Name: artistName, SortName: artistName})
if err != nil {
t.Fatalf("seed artist: %v", err)
}
artistIDs = append(artistIDs, ar.ID)
for k := 0; k < tracksPerArtist; k++ {
albumTitle := fmt.Sprintf("%s - Album %d", artistName, k/2)
al, err := q.UpsertAlbum(ctx, dbq.UpsertAlbumParams{
Title: albumTitle, SortTitle: albumTitle, ArtistID: ar.ID,
})
if err != nil {
t.Fatalf("seed album: %v", err)
}
tk, err := q.UpsertTrack(ctx, dbq.UpsertTrackParams{
Title: fmt.Sprintf("%s-t%d", artistName, k), AlbumID: al.ID, ArtistID: ar.ID,
DurationMs: 1000,
FilePath: filepath.Join(dir, fmt.Sprintf("%s-%d-%d.mp3", name, a, k)),
FileSize: 100, FileFormat: "mp3",
})
if err != nil {
t.Fatalf("seed track: %v", err)
}
// Well clear of the recently-played exclusion window, which uses
// the DATABASE clock rather than the build's `now`.
for pl := 0; pl < 3; pl++ {
seedPlayEvent(t, pool, u.ID, tk.ID,
now.Add(-time.Duration(24+a*10+k+pl)*time.Hour), false)
}
}
}
return u, artistIDs
}
// The seed artist's own tracks are ELIGIBLE for its "Songs like" mix, and are // The seed artist's own tracks are ELIGIBLE for its "Songs like" mix, and are
// bounded by the diversity cap rather than excluded outright (#3881). // bounded by the diversity cap rather than excluded outright (#3881).
// //
@@ -301,7 +365,7 @@ func TestBuildSystemPlaylists_Concurrency(t *testing.T) {
func TestBuildSystemPlaylists_SongsLikeIncludesItsSeedArtist(t *testing.T) { func TestBuildSystemPlaylists_SongsLikeIncludesItsSeedArtist(t *testing.T) {
pool := newPool(t) pool := newPool(t)
logger := discardLogger() logger := discardLogger()
u, _ := seedActiveLibrary(t, pool, "seedartist", 4, 5) u, _ := seedSharedArtistLibrary(t, pool, "seedartist", 4, 6)
ctx := context.Background() ctx := context.Background()
now := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC)