fix: persist all found candidates and guard duplicate adapter names

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-10 18:54:15 +02:00
parent 71c7945da2
commit 7c28eddd47
2 changed files with 41 additions and 5 deletions
+12 -5
View File
@@ -1,8 +1,10 @@
from dataclasses import replace
from typing import Sequence from typing import Sequence
import psycopg import psycopg
from lyra_worker.adapters.base import SourceAdapter from lyra_worker.adapters.base import SourceAdapter
from lyra_worker.confidence import score_confidence
from lyra_worker.quality import quality_class from lyra_worker.quality import quality_class
from lyra_worker.ranker import rank_candidates from lyra_worker.ranker import rank_candidates
from lyra_worker.types import Candidate, MBTarget from lyra_worker.types import Candidate, MBTarget
@@ -103,6 +105,10 @@ def run_pipeline(
dest_root: str = "/music", dest_root: str = "/music",
) -> None: ) -> None:
"""Real staged acquisition using source-agnostic adapters. Fakes in this plan.""" """Real staged acquisition using source-agnostic adapters. Fakes in this plan."""
names = [a.name for a in adapters]
if len(names) != len(set(names)):
raise ValueError(f"adapter names must be unique, got {names}")
# 1. intake # 1. intake
_set_state(conn, job_id, "matching", "intake") _set_state(conn, job_id, "matching", "intake")
target = _load_target(conn, job_id) target = _load_target(conn, job_id)
@@ -116,16 +122,17 @@ def run_pipeline(
conn.commit() conn.commit()
return return
# 2. match # 2. match — search all adapters; score + persist EVERY candidate found
_set_state(conn, job_id, "matching", "match") _set_state(conn, job_id, "matching", "match")
candidates: list[Candidate] = [] found: list[Candidate] = []
for adapter in adapters: for adapter in adapters:
candidates.extend(adapter.search(target)) for c in adapter.search(target):
found.append(replace(c, confidence=score_confidence(target, c)))
_persist_candidates(conn, job_id, found)
# 3. rank # 3. rank
_set_state(conn, job_id, "matched", "rank") _set_state(conn, job_id, "matched", "rank")
ranked = rank_candidates(target, candidates, min_confidence) ranked = rank_candidates(target, found, min_confidence)
_persist_candidates(conn, job_id, ranked)
if not ranked: if not ranked:
_fail(conn, job_id, "no candidate above confidence threshold") _fail(conn, job_id, "no candidate above confidence threshold")
return return
+29
View File
@@ -99,3 +99,32 @@ def test_dedupe_skips_already_in_library(conn):
cur.execute('SELECT count(*) FROM "LibraryItem" WHERE artist = %s AND album = %s', cur.execute('SELECT count(*) FROM "LibraryItem" WHERE artist = %s AND album = %s',
("Radiohead", "In Rainbows")) ("Radiohead", "In Rainbows"))
assert cur.fetchone()[0] == 1 # not duplicated assert cur.fetchone()[0] == 1 # not duplicated
def test_below_threshold_candidates_are_persisted_for_diagnosis(conn):
from lyra_worker.adapters.fakes import FakeQobuz
from lyra_worker.types import MBTarget
class WrongAlbumQobuz(FakeQobuz):
def search(self, target):
# returns a candidate for a different album -> scores below the gate
return super().search(MBTarget(artist=target.artist, album="Some Completely Unrelated Title"))
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
run_pipeline(conn, job_id, [WrongAlbumQobuz()], dest_root="/tmp/lib")
assert _job_state(conn, job_id)[0] == "needs_attention"
with conn.cursor() as cur:
cur.execute('SELECT count(*) FROM "Candidate" WHERE "jobId" = %s', (job_id,))
assert cur.fetchone()[0] >= 1 # found-but-rejected candidate retained for diagnosis
def test_duplicate_adapter_names_raise(conn):
import pytest
from lyra_worker.adapters.fakes import FakeQobuz
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
with pytest.raises(ValueError):
run_pipeline(conn, job_id, [FakeQobuz(), FakeQobuz()], dest_root="/tmp/lib")