Files
Lyra/worker/tests/test_monitor_reconcile.py
T
Jonathan 082365005c fix(worker): reconcile every job + backfill owned releases
Wanted releases pressed while the monitor is off never got reconciled, so
they lingered on the wanted list. Extract finish_job() and call it for every
finished job (not just when monitor.enabled) so the MonitoredRelease
transitions to fulfilled. Add fulfill_owned_releases(), run once at startup,
to backfill releases already owned at/above the quality cutoff (self-heals
items stuck from before this fix). Both idempotent + tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:24 +02:00

124 lines
5.3 KiB
Python

from lyra_worker.monitor import MonitorConfig, fulfill_owned_releases, reconcile
from tests.conftest import insert_monitored_release
CFG = MonitorConfig(enabled=True, quality_cutoff=2)
def _make_job(conn, rel_id, artist, album, state):
with conn.cursor() as cur:
cur.execute(
'INSERT INTO "Request" (id, artist, album, status, "monitoredReleaseId", "createdAt") '
"VALUES (gen_random_uuid()::text, %s, %s, 'pending', %s, now()) RETURNING id",
(artist, album, rel_id),
)
req = cur.fetchone()[0]
cur.execute(
'INSERT INTO "Job" (id, "requestId", state, "currentStage", attempts, "createdAt", "updatedAt") '
"VALUES (gen_random_uuid()::text, %s, %s, 'import', 1, now(), now()) RETURNING id",
(req, state),
)
job = cur.fetchone()[0]
conn.commit()
return req, job
def _add_library(conn, req, artist, album, quality):
with conn.cursor() as cur:
cur.execute(
'INSERT INTO "LibraryItem" (id, "requestId", artist, album, path, source, format, '
'"qualityClass", "importedAt") '
"VALUES (gen_random_uuid()::text, %s, %s, %s, '/m', 'qobuz', 'FLAC', %s, now())",
(req, artist, album, quality),
)
conn.commit()
def _release(conn, rel_id):
with conn.cursor() as cur:
cur.execute(
'SELECT state, "currentQualityClass", "firstGrabbedAt" IS NOT NULL '
'FROM "MonitoredRelease" WHERE id = %s',
(rel_id,),
)
return cur.fetchone()
def test_imported_below_cutoff_becomes_grabbed(conn):
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
req, job = _make_job(conn, rel, "A", "B", "imported")
_add_library(conn, req, "A", "B", 1)
reconcile(conn, job, CFG)
assert _release(conn, rel) == ("grabbed", 1, True)
def test_imported_at_cutoff_becomes_fulfilled(conn):
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
req, job = _make_job(conn, rel, "A", "B", "imported")
_add_library(conn, req, "A", "B", 3)
reconcile(conn, job, CFG)
assert _release(conn, rel) == ("fulfilled", 3, True)
def test_needs_attention_leaves_wanted(conn):
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
_req, job = _make_job(conn, rel, "A", "B", "needs_attention")
reconcile(conn, job, CFG)
assert _release(conn, rel) == ("wanted", None, False)
def test_finish_job_reconciles_even_when_monitor_disabled(conn):
# Regression: a manually-wanted release that gets pressed must transition to fulfilled
# (and drop off the Wanted list) even with the monitor off — reconcile is core
# correctness, not a monitor-sweep feature.
from lyra_worker.main import finish_job
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
req, job = _make_job(conn, rel, "A", "B", "imported")
_add_library(conn, req, "A", "B", 3)
finish_job(conn, job, MonitorConfig(enabled=False, quality_cutoff=2))
assert _release(conn, rel) == ("fulfilled", 3, True)
def test_fulfill_owned_releases_backfills_stuck_wanted(conn):
# A release pressed while the monitor was off: owned in the library at cutoff, but its
# MonitoredRelease is still 'wanted' (never reconciled). Startup backfill fixes it.
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
other = insert_monitored_release(conn, artist_name="C", album="D", rg_mbid="rg-2",
monitored=True, state="wanted") # not owned → stays wanted
_add_library(conn, None, "A", "B", 3)
cfg = MonitorConfig(quality_cutoff=2)
assert fulfill_owned_releases(conn, cfg) == 1
assert _release(conn, rel) == ("fulfilled", 3, True)
assert _release(conn, other)[0] == "wanted"
assert fulfill_owned_releases(conn, cfg) == 0 # idempotent
def test_fulfill_owned_releases_ignores_below_cutoff(conn):
rel = insert_monitored_release(conn, artist_name="A", album="B", rg_mbid="rg-1",
monitored=True, state="wanted")
_add_library(conn, None, "A", "B", 1) # owned but below cutoff → keep wanting (upgrade)
assert fulfill_owned_releases(conn, MonitorConfig(quality_cutoff=2)) == 0
assert _release(conn, rel)[0] == "wanted"
def test_unlinked_job_is_ignored(conn):
with conn.cursor() as cur:
cur.execute(
'INSERT INTO "Request" (id, artist, album, status, "createdAt") '
"VALUES (gen_random_uuid()::text, 'A', 'B', 'completed', now()) RETURNING id"
)
req = cur.fetchone()[0]
cur.execute(
'INSERT INTO "Job" (id, "requestId", state, "currentStage", attempts, "createdAt", "updatedAt") '
"VALUES (gen_random_uuid()::text, %s, 'imported', 'import', 1, now(), now()) RETURNING id",
(req,),
)
job = cur.fetchone()[0]
conn.commit()
reconcile(conn, job, CFG) # no monitoredReleaseId → no-op, must not raise