diff --git a/cron/jobs.py b/cron/jobs.py index d359adb522..9e7a507251 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -1377,13 +1377,17 @@ def load_jobs() -> List[Dict[str, Any]]: 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. + # Types only: the raw values are arbitrary file content and must not reach the logs. 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]))) + "Skipping %d non-object entr%s in jobs.json (types: %s)", + len(junk), "y" if len(junk) == 1 else "ies", + ", ".join(sorted({type(j).__name__ for j in junk}))) jobs = [j for j in jobs if isinstance(j, dict)] repair = repair or "non-object entries dropped" - if jobs and repair: + # Persist even an empty result, or an all-junk store repeats the repair on every tick; the + # save's shrink-merge still keeps any valid job a sibling wrote meanwhile. + if repair: save_jobs(jobs) logger.warning("Auto-repaired jobs.json (%s)", repair) _record_load_stamp(pre_read_stamp) diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index d301c48464..5043d3c5b0 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -1735,6 +1735,20 @@ class TestJobsJsonIdKeyedMap: on_disk = json.loads(JOBS_FILE.read_text(encoding="utf-8")) assert [j["id"] for j in on_disk["jobs"]] == [job["id"]] + def test_all_junk_list_is_repaired_on_disk_without_logging_values(self, tmp_cron_dir, caplog): + """With no valid job left the repair must still persist (else every tick repeats it), and + the warning must not copy raw file content into the logs.""" + import json + from cron.jobs import JOBS_FILE, ensure_dirs + + ensure_dirs() + JOBS_FILE.write_text(json.dumps({"jobs": [None, "sk-leaked-value", 42]}), encoding="utf-8") + + with caplog.at_level("WARNING", logger="cron.jobs"): + assert list_jobs(include_disabled=True) == [] + assert json.loads(JOBS_FILE.read_text(encoding="utf-8"))["jobs"] == [] + assert "sk-leaked-value" not in caplog.text +