package explore import ( "log/slog" "testing" "yellowjacket/backend/database" ) // seedIndexArtist inserts an explore_index artist row, optionally // already marked as having had its ListenBrainz discography fetched. func seedIndexArtist(t *testing.T, db *database.DB, mbid string, discogFetched int) { t.Helper() seedIndexResult(t, db, SearchIndexResult{ EntityType: EntityArtist, MBID: testMBID(mbid), Title: "Seeded Artist", ArtistName: "Seeded Artist", ArtistMBID: testMBID(mbid), InLibrary: true, DiscogFetched: discogFetched == 1, }) } // TestArtistEnrichmentMarksAreIndependent is the reason these are two // columns rather than one boolean: each fetch fails on its own, and one // failing must not claim the other as done. func TestArtistEnrichmentMarksAreIndependent(t *testing.T) { t.Parallel() db := database.NewTestDB(t) si := NewSearchIndex(db, nil, nil, slog.Default()) const mbid = "11111111-1111-1111-1111-111111111111" if mark := si.enrichmentFor(mbid); mark.Browsed || mark.Similar { t.Fatalf("unmarked artist reported as enriched: %+v", mark) } si.markArtistBrowsed(mbid) mark := si.enrichmentFor(mbid) if !mark.Browsed { t.Error("browsed_at was not recorded") } if mark.Similar { t.Error("marking browsed also marked similar") } // The second mark upserts onto the same row rather than replacing it. si.markArtistSimilar(mbid) mark = si.enrichmentFor(mbid) if !mark.Browsed || !mark.Similar { t.Errorf("marking similar lost the browsed mark: %+v", mark) } if !si.artistBrowsed(mbid) { t.Error("artistBrowsed disagreed with enrichmentFor") } } // TestUnenrichedIncludesBrowsedGap is the query change 011 turns on: an // artist the catalog artifact already covers (discog_fetched = 1) has // still never been browsed, and the artifact's per-artist coverage is // graded — so "the artifact knows them" is not "we have their // discography", and the backfill must still pick them up. func TestUnenrichedIncludesBrowsedGap(t *testing.T) { t.Parallel() db := database.NewTestDB(t) si := NewSearchIndex(db, nil, nil, slog.Default()) const mbid = "22222222-2222-2222-2222-222222222222" if _, err := db.ExecContext( "INSERT INTO artists (name, mbid) VALUES (?, ?)", "Owned Artist", mbid, ); err != nil { t.Fatalf("seed library artist: %v", err) } seedIndexArtist(t, db, mbid, 1) mbids := si.unenrichedLibraryArtistMBIDs(10) if !containsMBID(mbids, mbid) { t.Fatalf("artist with discog_fetched=1 but no browse was skipped: %v", mbids) } // Both marks set: nothing left to do, so it drops out entirely. si.markArtistBrowsed(mbid) si.markArtistSimilar(mbid) if mbids := si.unenrichedLibraryArtistMBIDs(10); containsMBID(mbids, mbid) { t.Errorf("fully enriched artist was returned again: %v", mbids) } } // TestUnenrichedIgnoresSimilarMark pins the other half of that query: // the backfill no longer fetches similar artists (the artist page does, // on view), so an artist it has finished with must drop out even though // similar_at is still NULL. Testing a mark nothing in the pass sets // makes every owned artist a candidate on every run, forever — which is // a backfill that reports progress and never converges. func TestUnenrichedIgnoresSimilarMark(t *testing.T) { t.Parallel() db := database.NewTestDB(t) si := NewSearchIndex(db, nil, nil, slog.Default()) const mbid = "33333333-3333-3333-3333-333333333333" if _, err := db.ExecContext( "INSERT INTO artists (name, mbid) VALUES (?, ?)", "Owned Artist", mbid, ); err != nil { t.Fatalf("seed library artist: %v", err) } seedIndexArtist(t, db, mbid, 1) si.markArtistBrowsed(mbid) if mbids := si.unenrichedLibraryArtistMBIDs(10); containsMBID(mbids, mbid) { t.Errorf("artist with no similar_at was returned again: %v", mbids) } } func containsMBID(mbids []string, want string) bool { for _, m := range mbids { if m == want { return true } } return false }