"""_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"}, ] def test_a_failure_the_job_names_rides_along_as_data(db, make_user): """E31: a failed RSS poll reached the Hungarian audit log as the server's English sentence. A failure the job names (`progress.JobFailed`) is stored as data beside that sentence — on the dashboard, the manual-run notice and the audit row — so the client says it in the reader's language. Any other failure keeps only its own text (the test above).""" from app import progress from app.models import AuditLog def outage(_db): raise progress.JobFailed("rss_all_failed", "RSS poll failed", total=2, cause="transport") admin = make_user("failure@example.com", role="admin") token = scheduler._manual_actor.set(admin.id) try: scheduler._job("rss_poll", outage) finally: scheduler._manual_actor.reset(token) named = {"failure": "rss_all_failed", "total": 2, "cause": "transport"} rss = _snapshot_job("rss_poll") assert rss["last_error"] == "RSS poll failed" assert rss["result"] == named notice = db.execute(select(Notification).where(Notification.user_id == admin.id)).scalar_one() assert notice.data["result"] == named row = db.execute(select(AuditLog).order_by(AuditLog.id.desc())).scalars().first() assert row is not None and row.after["result"] == named