"""_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