from lyra_worker.adapters.fakes import FakeMbBrowser, FakeSimilaritySource from lyra_worker.discovery import DiscoveryConfig, run_discovery from lyra_worker.similarity.base import SimilarArtist from tests.conftest import insert_discovery_suggestion, insert_watched_artist def _artist_rows(conn): with conn.cursor() as cur: cur.execute('SELECT "artistMbid", "artistName", score, "seedCount", sources, status::text ' 'FROM "DiscoverySuggestion" WHERE kind = \'artist\' ORDER BY score DESC') return cur.fetchall() def test_aggregates_scores_across_seeds(conn): insert_watched_artist(conn, mbid="s1", name="Seed One") insert_watched_artist(conn, mbid="s2", name="Seed Two") src = FakeSimilaritySource(similar={ "s1": [SimilarArtist("c1", "Shared", 0.6), SimilarArtist("c2", "Only1", 0.4)], "s2": [SimilarArtist("c1", "Shared", 0.5)], }) result = run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()) assert result.artists == 2 rows = _artist_rows(conn) # c1 recommended by both seeds: score 1.1, seedCount 2; c2 by one seed assert rows[0][0] == "c1" and abs(rows[0][2] - 1.1) < 1e-6 and rows[0][3] == 2 assert rows[0][4] == ["fake"] and rows[0][5] == "pending" assert rows[1][0] == "c2" and rows[1][3] == 1 def test_skips_already_followed_artists(conn): insert_watched_artist(conn, mbid="s1", name="Seed") insert_watched_artist(conn, mbid="c1", name="Already Followed") src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "X", 0.9), SimilarArtist("c2", "New", 0.8)]}) run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()) assert [r[0] for r in _artist_rows(conn)] == ["c2"] # c1 filtered (followed) def test_min_score_filters_weak_candidates(conn): insert_watched_artist(conn, mbid="s1", name="Seed") src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Strong", 0.9), SimilarArtist("c2", "Weak", 0.1)]}) run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig(min_score=0.5)) assert [r[0] for r in _artist_rows(conn)] == ["c1"] def test_dismissed_suggestion_is_never_revived(conn): insert_watched_artist(conn, mbid="s1", name="Seed") insert_discovery_suggestion(conn, kind="artist", artist_mbid="c1", status="dismissed", score=0.0) src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "X", 0.9)]}) run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()) with conn.cursor() as cur: cur.execute('SELECT status::text, score FROM "DiscoverySuggestion" WHERE "artistMbid" = %s', ("c1",)) assert cur.fetchone() == ("dismissed", 0.0) # untouched — WHERE status='pending' blocked the update def test_stamps_last_discovered_and_throttles(conn): insert_watched_artist(conn, mbid="s1", name="Seed") src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "X", 0.9)]}) assert run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()).artists == 1 with conn.cursor() as cur: cur.execute('SELECT "lastDiscoveredAt" IS NOT NULL FROM "WatchedArtist" WHERE mbid = %s', ("s1",)) assert cur.fetchone()[0] is True # second run: seed not due (lastDiscoveredAt recent) -> no seeds -> nothing new assert run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()).artists == 0 def test_unhealthy_source_contributes_nothing(conn): insert_watched_artist(conn, mbid="s1", name="Seed") src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "X", 0.9)]}, healthy=False) assert run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig()).artists == 0 assert _artist_rows(conn) == [] def test_source_with_raising_health_is_skipped_not_fatal(conn): insert_watched_artist(conn, mbid="s1", name="Seed") class BoomSource: name = "boom" def health(self): raise RuntimeError("boom") def similar_artists(self, mbid): return [] good = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "X", 0.9)]}) result = run_discovery(conn, [BoomSource(), good], FakeMbBrowser(), DiscoveryConfig()) assert result.artists == 1 # good source still processed; boom skipped, no crash def test_config_from_config_parses_and_defaults(): cfg = DiscoveryConfig.from_config({"discover.enabled": "true", "discover.chunkSize": "10", "discover.minScore": "0.3"}) assert cfg.enabled is True and cfg.chunk_size == 10 and cfg.min_score == 0.3 d = DiscoveryConfig.from_config({}) assert (d.enabled, d.interval_hours, d.chunk_size, d.similar_per_seed, d.albums_per_artist, d.min_score) == (False, 168, 5, 20, 1, 0.0) def test_run_discovery_processes_at_most_chunk_size_seeds(conn): from lyra_worker.discovery import DiscoveryConfig, run_discovery from lyra_worker.adapters.fakes import FakeMbBrowser, FakeSimilaritySource from lyra_worker.similarity.base import SimilarArtist from tests.conftest import insert_watched_artist for i in range(4): insert_watched_artist(conn, mbid=f"s{i}", name=f"Seed{i}") src = FakeSimilaritySource(similar={f"s{i}": [SimilarArtist(f"c{i}", f"Cand{i}", 0.9)] for i in range(4)}) cfg = DiscoveryConfig.from_config({"discover.chunkSize": "2"}) result = run_discovery(conn, [src], FakeMbBrowser(), cfg) assert result.seeds == 2 # only chunk_size seeds this call # exactly 2 seeds now have lastDiscoveredAt set with conn.cursor() as cur: cur.execute('SELECT count(*) FROM "WatchedArtist" WHERE "lastDiscoveredAt" IS NOT NULL') assert cur.fetchone()[0] == 2