refactor(e5): deferred review-tail (backend) — enrich off request path, reuse, tests
Address the deep-review's deferred backend items so E5 is review-complete: - person-enrich OFF the request path (ABUSE finding): person_detail no longer makes the TMDB call inline (even bounded to 8s it sat on the serve thread, un-throttled, demo-reachable — it violated the module's "no live TMDB call at serve time" invariant). It now schedules a FastAPI BackgroundTask (people._enrich_person_bg, fresh session, best-effort) after the response; the first open serves the row as-is and the bio lands by the next open. The inline try/rollback recovery is gone with it. - reuse: a shared tmdb.resolve_library_guids owns the collision-safe guid→library bucketing (movie vs tv ids are independent sequences) that resolve_recs and people._resolve_known_for each hand-duplicated; a new rollup.item_state_map dedups the "PlexState-by-item_id" fetch across the person + discover paths. - test gaps: enrich_batch transient TmdbError leaves the watermark NULL to retry; guid_to_tmdb_id resolves a tvdb source + tv media_type via /find's tv_results; _continue_watching includes a NULL-status started movie and excludes a hidden one; /api/search/youtube returns 422 (not 5xx) when ingest fails (route-level, B-core-1). Backend lane 403 → 413, ruff clean. No migration; serve contracts unchanged.
This commit is contained in:
@@ -100,34 +100,51 @@ def _get(db, user, monkeypatch, path):
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_person_detail_survives_poisoned_enrich(db, monkeypatch):
|
||||
# If the lazy enrich_person leaves the session in a failed-transaction state (e.g. a commit-time
|
||||
# flush error), person_detail's except must roll it back so the following SELECTs still run — the
|
||||
# page renders despite the enrich failure ("never fail the page"). Without the db.rollback() fix the
|
||||
# poisoned session would raise PendingRollbackError on the next query and 500.
|
||||
from sqlalchemy import text
|
||||
def test_person_detail_schedules_enrich_off_request_path(db, monkeypatch):
|
||||
# The bio fetch runs OFF the request path (a background task after the response), so the serve never
|
||||
# makes a live TMDB call. An un-enriched person (tmdb_person_id set + tmdb_enriched_at None) schedules
|
||||
# the enrich and the page still renders immediately with whatever's in the row (bio still null).
|
||||
from app.routes.plex import people
|
||||
|
||||
lib = PlexLibrary(plex_key="9", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
db.flush()
|
||||
# Un-enriched person (tmdb_person_id set + tmdb_enriched_at None) → triggers the lazy enrich path.
|
||||
db.add(PlexPerson(name="Poison P", tmdb_person_id=777))
|
||||
user = User(email="poison@example.com")
|
||||
db.add(PlexPerson(name="Lazy P", tmdb_person_id=777))
|
||||
user = User(email="lazybg@example.com")
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
def _poison_then_raise(db_, person_):
|
||||
try:
|
||||
db_.execute(text("SELECT 1 / 0")) # aborts the transaction (like a failed enrich commit)
|
||||
except Exception: # noqa: BLE001 — swallow so the tx stays poisoned, then raise like the real path
|
||||
pass
|
||||
raise RuntimeError("enrich commit blew up")
|
||||
scheduled: list[int] = []
|
||||
monkeypatch.setattr(people, "_enrich_person_bg", lambda pid: scheduled.append(pid))
|
||||
|
||||
monkeypatch.setattr(plex_tmdb, "enrich_person", _poison_then_raise)
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/person/Lazy P")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["biography"] is None # not enriched yet — the bio lands by the next open
|
||||
assert len(scheduled) == 1 # the enrich was scheduled off the request path
|
||||
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/person/Poison P")
|
||||
assert resp.status_code == 200, resp.text # page still renders (session was rolled back)
|
||||
assert resp.json()["biography"] is None # enrich failed, so no bio
|
||||
|
||||
def test_person_detail_no_enrich_when_already_enriched(db, monkeypatch):
|
||||
# An already-enriched person (tmdb_enriched_at set) must NOT schedule another background fetch.
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.routes.plex import people
|
||||
|
||||
lib = PlexLibrary(plex_key="8", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
db.flush()
|
||||
db.add(PlexPerson(name="Done P", tmdb_person_id=888,
|
||||
tmdb_enriched_at=datetime(2024, 1, 1, tzinfo=timezone.utc), biography="Bio."))
|
||||
user = User(email="donebg@example.com")
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
scheduled: list[int] = []
|
||||
monkeypatch.setattr(people, "_enrich_person_bg", lambda pid: scheduled.append(pid))
|
||||
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/person/Done P")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["biography"] == "Bio."
|
||||
assert scheduled == [] # already enriched → no background work
|
||||
|
||||
|
||||
def test_person_detail_serves_bio_and_resolves_known_for(db, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user