- people_enrich: a failed TMDB lookup or a dead headshot is skipped, not the end of the run (a dead file is marked and left for a week; three failures in a row end it); portraits wait for the Plex module switch; a no-Plex original keeps its own cache key, so the resize still replaces it; the job entry lives in the plex domain (run_people_enrich) - person-image takes only a headshot-shaped CDN url (no query variants of one file) - every synced row gets Hungarian genres, and a HU genre filter matches the word as picked too; migration 0094 fills the rows no overlay reached - a job run's audit row carries its result as data; audit_clear / demo_reset store their counts - one credit-column list for the prune and the enrichment order; _overlay_pass typed - tests: batch skip + stop, HU bio stored, dead headshots, module switch, the person-image route, the job body, genre filter + facet write + twin fallback, tagline/summary fallbacks, audit data, the reader's alphabet at every remaining collated() call site
89 lines
3.7 KiB
Python
89 lines
3.7 KiB
Python
"""_claim_running is the single-run guard that both the scheduled fire and the manual trigger go
|
|
through (at the top of _job): holding _activity_lock across the check-and-set means exactly one
|
|
caller can hold a job's "running" claim at a time, and it's claimable again once released."""
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app import scheduler
|
|
from app.models import Notification
|
|
|
|
_JOB = "unit_test_claim" # not a real job id — exercises the primitive without touching live jobs
|
|
|
|
|
|
def _release():
|
|
scheduler._record(_JOB, running=False)
|
|
|
|
|
|
def test_claim_is_exclusive_until_released():
|
|
_release()
|
|
# First caller claims it.
|
|
assert scheduler._claim_running(_JOB) is True
|
|
# A second caller is refused while the first still holds it (the TOCTOU the fix closes).
|
|
assert scheduler._claim_running(_JOB) is False
|
|
assert scheduler._claim_running(_JOB) is False
|
|
# _job's finally releases the claim...
|
|
_release()
|
|
# ...and now it can be claimed again.
|
|
assert scheduler._claim_running(_JOB) is True
|
|
_release()
|
|
|
|
|
|
# ---- E30: the result as data -------------------------------------------------------------------
|
|
|
|
|
|
def _snapshot_job(job_id):
|
|
return next(j for j in scheduler.scheduler_snapshot()["jobs"] if j["id"] == job_id)
|
|
|
|
|
|
def test_a_run_keeps_its_result_as_data_for_the_readers_language(db, make_user):
|
|
"""The dashboard and the manual-run notice showed `_summarize`'s English one-liner ("new=3,
|
|
failed=0", a Python repr for a nested phase). The result rides along as data, so the client names
|
|
it; the one-liner stays as the fallback and the audit text."""
|
|
admin = make_user("sched@example.com", role="admin")
|
|
token = scheduler._manual_actor.set(admin.id)
|
|
try:
|
|
scheduler._job("rss_poll", lambda _db: {"new": 3, "failed": 0, "fallback": 1})
|
|
scheduler._job("enrich", lambda _db: 5) # enrich returns a bare count
|
|
finally:
|
|
scheduler._manual_actor.reset(token)
|
|
|
|
rss = _snapshot_job("rss_poll")
|
|
assert rss["result"] == {"new": 3, "failed": 0, "fallback": 1}
|
|
assert rss["last_result"] == "new=3, failed=0, fallback=1"
|
|
assert _snapshot_job("enrich")["result"] == {"enriched": 5}
|
|
|
|
notices = db.execute(
|
|
select(Notification).where(Notification.user_id == admin.id).order_by(Notification.id)
|
|
).scalars()
|
|
assert [n.data["result"] for n in notices] == [
|
|
{"new": 3, "failed": 0, "fallback": 1},
|
|
{"enriched": 5},
|
|
]
|
|
|
|
|
|
def test_a_failed_run_drops_the_previous_result(db):
|
|
scheduler._job("rss_poll", lambda _db: {"new": 1})
|
|
scheduler._job("rss_poll", lambda _db: (_ for _ in ()).throw(RuntimeError("boom")))
|
|
rss = _snapshot_job("rss_poll")
|
|
assert rss["last_error"] == "boom"
|
|
assert rss["result"] is None
|
|
|
|
|
|
def test_a_job_runs_audit_row_carries_its_result_as_data(db):
|
|
"""E30 review: the audit log names a run's result in the reader's language, as the inbox does —
|
|
from data, not from the English one-liner (a nested phase there was a Python dict repr)."""
|
|
from app.models import AuditLog, User
|
|
|
|
admin = User(email="auditrun@example.com")
|
|
db.add(admin)
|
|
db.commit()
|
|
scheduler._audit_run(db, None, "people_enrich", "ok", "x", {"bios": {"enriched": 2}})
|
|
scheduler._audit_run(db, admin.id, "rss_poll", "skipped", "sync paused", scheduler._PAUSED)
|
|
scheduler._audit_run(db, admin.id, "rss_poll", "error", "boom", None)
|
|
rows = db.execute(select(AuditLog).order_by(AuditLog.id)).scalars().all()
|
|
assert [r.after for r in rows] == [
|
|
{"status": "ok", "summary": "x", "result": {"bios": {"enriched": 2}}},
|
|
{"status": "skipped", "summary": "sync paused", "result": {"skipped": "sync paused"}},
|
|
{"status": "error", "summary": "boom"},
|
|
]
|