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:
@@ -1,8 +1,10 @@
|
||||
from dataclasses import replace
|
||||
from typing import Sequence
|
||||
|
||||
import psycopg
|
||||
|
||||
from lyra_worker.adapters.base import SourceAdapter
|
||||
from lyra_worker.confidence import score_confidence
|
||||
from lyra_worker.quality import quality_class
|
||||
from lyra_worker.ranker import rank_candidates
|
||||
from lyra_worker.types import Candidate, MBTarget
|
||||
@@ -103,6 +105,10 @@ def run_pipeline(
|
||||
dest_root: str = "/music",
|
||||
) -> None:
|
||||
"""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
|
||||
_set_state(conn, job_id, "matching", "intake")
|
||||
target = _load_target(conn, job_id)
|
||||
@@ -116,16 +122,17 @@ def run_pipeline(
|
||||
conn.commit()
|
||||
return
|
||||
|
||||
# 2. match
|
||||
# 2. match — search all adapters; score + persist EVERY candidate found
|
||||
_set_state(conn, job_id, "matching", "match")
|
||||
candidates: list[Candidate] = []
|
||||
found: list[Candidate] = []
|
||||
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
|
||||
_set_state(conn, job_id, "matched", "rank")
|
||||
ranked = rank_candidates(target, candidates, min_confidence)
|
||||
_persist_candidates(conn, job_id, ranked)
|
||||
ranked = rank_candidates(target, found, min_confidence)
|
||||
if not ranked:
|
||||
_fail(conn, job_id, "no candidate above confidence threshold")
|
||||
return
|
||||
|
||||
@@ -99,3 +99,32 @@ def test_dedupe_skips_already_in_library(conn):
|
||||
cur.execute('SELECT count(*) FROM "LibraryItem" WHERE artist = %s AND album = %s',
|
||||
("Radiohead", "In Rainbows"))
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user