a15f4ae8bb
Adds registry.build_similarity_sources() (always constructs ListenBrainzSource; reachability is a per-run health() concern, not a startup gate) and wires discovery into the worker's main loop: a scheduled sweep gated on discover.enabled + DISCOVER_TICK_SECONDS, and a one-shot discover.requested trigger mirroring the existing scan.requested pattern. Also hardens run_discovery's health-check pass: a source whose health() raises is now logged and skipped instead of aborting the whole sweep (accepted finding from the Task-3 review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
4.7 KiB
Python
97 lines
4.7 KiB
Python
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.maxSeeds": "10",
|
|
"discover.minScore": "0.3"})
|
|
assert cfg.enabled is True and cfg.max_seeds == 10 and cfg.min_score == 0.3
|
|
d = DiscoveryConfig.from_config({})
|
|
assert (d.enabled, d.interval_hours, d.max_seeds, d.similar_per_seed,
|
|
d.albums_per_artist, d.min_score) == (False, 168, 50, 20, 1, 0.0)
|