diff --git a/cron/jobs.py b/cron/jobs.py index e8ece8abe8..d359adb522 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -1374,6 +1374,15 @@ def load_jobs() -> List[Dict[str, Any]]: else: raise RuntimeError( f"Cron database corrupted: expected {{'jobs': [...]}}, got {type(data).__name__}") + if isinstance(jobs, list) and not all(isinstance(j, dict) for j in jobs): + # Every reader and the due scan index records as dicts: one junk entry would crash the + # whole tick and freeze every healthy sibling job, so skip it like the id-keyed map does. + junk = [j for j in jobs if not isinstance(j, dict)] + logger.warning( + "Skipping %d non-object entr%s in jobs.json: %s", + len(junk), "y" if len(junk) == 1 else "ies", ", ".join(map(repr, junk[:5]))) + jobs = [j for j in jobs if isinstance(j, dict)] + repair = repair or "non-object entries dropped" if jobs and repair: save_jobs(jobs) logger.warning("Auto-repaired jobs.json (%s)", repair) diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index 34818dae70..d301c48464 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -1718,6 +1718,23 @@ class TestJobsJsonIdKeyedMap: assert isinstance(on_disk["jobs"], list) assert [j["id"] for j in on_disk["jobs"]] == ["goodjob1"] + def test_non_dict_list_entries_do_not_stop_healthy_jobs_firing(self, tmp_cron_dir): + """A junk entry in the canonical list shape must not abort the due scan for its + healthy siblings (it used to raise on every tick, so no job fired).""" + import json + from cron.jobs import JOBS_FILE + + job = create_job(prompt="keep me", schedule="every 1h", name="survivor") + payload = json.loads(JOBS_FILE.read_text(encoding="utf-8")) + payload["jobs"][0]["next_run_at"] = (_hermes_now() - timedelta(seconds=5)).isoformat() + payload["jobs"] += [None, "i am not a job", 42] + JOBS_FILE.write_text(json.dumps(payload), encoding="utf-8") + + assert [j["id"] for j in get_due_jobs()] == [job["id"]] + assert [j["id"] for j in list_jobs(include_disabled=True)] == [job["id"]] + on_disk = json.loads(JOBS_FILE.read_text(encoding="utf-8")) + assert [j["id"] for j in on_disk["jobs"]] == [job["id"]] +