Files
yellowjacket/backend/explore/prune_test.go
T
yonluandClaude Sonnet 5 cbd82a5a74
Build & publish Arch package / arch-package (push) Successful in 2m2s
Search index maintenance / maintain-index (push) Successful in 7s
feat: autotag mixed-bag splitting, search relevance fixes, and multi-library download imports
Autotag: detect "junk drawer" folders with no artist/album consensus
and split them into synthetic per-cluster groups instead of forcing
one match on an unrelated pile of tracks; repair tagging_items rows
left behind by a prior scan orphan-cleanup gap.

Explore: fix an exact artist-name search being drowned out by its own
catalog entries in intent-prior scoring, and prune stale in_library
bookkeeping left behind when a referenced library row is deleted.

Download: fix a multi-library regression where every import failed
with "no library root configured" — the importer resolved the
library root from a legacy single-library config field that nothing
populates in the current multi-library model. It now resolves the
destination library per-request from the request's own library_id.
Also widen the Soulseek search window (12s -> 20s), measured against
real request history to be missing available peers on live queries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
2026-08-10 11:52:26 -04:00

157 lines
4.5 KiB
Go

package explore
import (
"log/slog"
"testing"
"yellowjacket/backend/database"
"yellowjacket/backend/database/sql/sqlcgen"
)
// TestPruneStaleLocalCrossReferences verifies that an explore_index row
// whose local_*_id points at a library row that no longer exists gets its
// in_library flag and local_*_id cleared, while a row still backed by a
// real library entity is left untouched. This is the fix for stale
// "in_library" bookkeeping surviving a rescan that removed the file it
// was tied to (upsertIndexConflictSQL only ever adds these references,
// never clears them).
func TestPruneStaleLocalCrossReferences(t *testing.T) {
t.Parallel()
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, slog.Default())
// A library artist that still exists.
artist, err := db.Queries.UpsertArtist(t.Context(), "Still Owned")
if err != nil {
t.Fatalf("upsert artist: %v", err)
}
// Two explore_index artist rows: one pointing at the still-existing
// artist, one pointing at a local ID that has since been deleted
// (simulating a rescan that removed the owning artist).
seedArtist := func(mbid, title string, localID int64) {
t.Helper()
if _, err := db.ExecContext(
upsertIndexSQL,
"artist", mbid, title, title, mbid, "",
0, 0,
0, "", "",
"", "", "",
"", "", "", "",
1, 0,
localID, 0, 0,
0,
); err != nil {
t.Fatalf("seed explore_index row for %q: %v", mbid, err)
}
}
seedArtist("still-owned-mbid", "Still Owned", artist.ID)
seedArtist("removed-mbid", "Removed Artist", 999999)
si.pruneStaleLocalCrossReferences()
stillOwned := si.LookupArtistByMBID("still-owned-mbid")
if stillOwned == nil {
t.Fatal("expected still-owned artist row to survive pruning")
}
if !stillOwned.InLibrary || stillOwned.LocalArtistID != artist.ID {
t.Errorf(
"still-owned artist: InLibrary=%v LocalArtistID=%d, want InLibrary=true LocalArtistID=%d",
stillOwned.InLibrary,
stillOwned.LocalArtistID,
artist.ID,
)
}
removed := si.LookupArtistByMBID("removed-mbid")
if removed == nil {
t.Fatal("expected removed-artist row to still exist (only cross-references cleared)")
}
if removed.InLibrary || removed.LocalArtistID != 0 {
t.Errorf(
"removed artist: InLibrary=%v LocalArtistID=%d, want InLibrary=false LocalArtistID=0",
removed.InLibrary, removed.LocalArtistID,
)
}
}
// TestUnenrichedLibraryArtistMBIDs_OrdersByOwnedTrackCount verifies the
// backfill queue prioritizes artists by how many tracks the user actually
// owns, not by how many duplicate-mbid artist rows happen to exist (the
// previous "ORDER BY COUNT(*)" grouped on a.mbid, which is nearly always 1
// per artist and so wasn't really ordering by anything meaningful).
func TestUnenrichedLibraryArtistMBIDs_OrdersByOwnedTrackCount(t *testing.T) {
t.Parallel()
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, slog.Default())
q := db.Queries
ctx := t.Context()
seedArtistWithTracks := func(name, mbid string, trackCount int) {
t.Helper()
artist, err := q.UpsertArtist(ctx, name)
if err != nil {
t.Fatalf("upsert artist %q: %v", name, err)
}
_, err = db.ExecContext("UPDATE artists SET mbid = ? WHERE id = ?", mbid, artist.ID)
if err != nil {
t.Fatalf("set mbid for %q: %v", name, err)
}
ac, err := q.UpsertArtistCredit(ctx, name)
if err != nil {
t.Fatalf("upsert artist credit %q: %v", name, err)
}
if _, err := q.CreateArtistCreditArtist(ctx, sqlcgen.CreateArtistCreditArtistParams{
ArtistID: artist.ID,
CreditID: ac.ID,
}); err != nil {
t.Fatalf("link artist credit artist %q: %v", name, err)
}
for i := range trackCount {
rec, err := q.CreateRecordingFull(ctx, sqlcgen.CreateRecordingFullParams{
Name: name,
ArtistCreditID: ac.ID,
})
if err != nil {
t.Fatalf("create recording for %q: %v", name, err)
}
if _, err := q.CreateAudioFile(ctx, sqlcgen.CreateAudioFileParams{
FilePath: name + "/" + string(rune('a'+i)) + ".mp3",
LengthMilliseconds: 180000,
RecordingID: rec.ID,
Basename: string(rune('a'+i)) + ".mp3",
}); err != nil {
t.Fatalf("create audio file for %q: %v", name, err)
}
}
}
seedArtistWithTracks("Few Tracks", "few-mbid", 1)
seedArtistWithTracks("Many Tracks", "many-mbid", 5)
mbids := si.unenrichedLibraryArtistMBIDs(10)
if len(mbids) != 2 {
t.Fatalf("unenrichedLibraryArtistMBIDs() = %v, want 2 entries", mbids)
}
if mbids[0] != "many-mbid" {
t.Errorf(
"unenrichedLibraryArtistMBIDs()[0] = %q, want %q (most owned tracks first)",
mbids[0],
"many-mbid",
)
}
}