diff --git a/nerve/cron/jobs.py b/nerve/cron/jobs.py index 79201df8..44a1a4f3 100644 --- a/nerve/cron/jobs.py +++ b/nerve/cron/jobs.py @@ -201,9 +201,25 @@ def load_jobs(jobs_file: Path) -> list[CronJob]: logger.error("Failed to load cron jobs from %s: %s", jobs_file, e) return [] - jobs_data = data.get("jobs", []) if isinstance(data, list): jobs_data = data + elif isinstance(data, dict): + jobs_data = data.get("jobs", []) + else: + logger.warning( + "Invalid cron jobs file %s: expected a mapping or list, got %s", + jobs_file, + type(data).__name__, + ) + return [] + + if not isinstance(jobs_data, list): + logger.warning( + "Invalid cron jobs file %s: expected 'jobs' to be a list, got %s", + jobs_file, + type(jobs_data).__name__, + ) + return [] jobs = [] for item in jobs_data: diff --git a/tests/test_cron.py b/tests/test_cron.py index d1614bdc..dea12c09 100644 --- a/tests/test_cron.py +++ b/tests/test_cron.py @@ -972,6 +972,46 @@ async def test_and_semantics_one_gate_blocks(self, cron_service): cron_service.engine.run_cron.assert_not_called() +# --------------------------------------------------------------------------- +# Job loading +# --------------------------------------------------------------------------- + +class TestLoadJobs: + def test_accepts_top_level_list(self, tmp_path): + from nerve.cron.jobs import load_jobs + + yaml_file = tmp_path / "jobs.yaml" + yaml_file.write_text( + "- id: hourly-review\n" + " schedule: 1h\n" + " prompt: Review pending tasks\n", + encoding="utf-8", + ) + + jobs = load_jobs(yaml_file) + + assert len(jobs) == 1 + assert jobs[0].id == "hourly-review" + assert jobs[0].schedule == "1h" + assert jobs[0].prompt == "Review pending tasks" + + @pytest.mark.parametrize( + ("content", "message"), + [ + ("invalid\n", "expected a mapping or list, got str"), + ("jobs: invalid\n", "expected 'jobs' to be a list, got str"), + ], + ) + def test_rejects_invalid_structure(self, tmp_path, caplog, content, message): + from nerve.cron.jobs import load_jobs + + yaml_file = tmp_path / "jobs.yaml" + yaml_file.write_text(content, encoding="utf-8") + + assert load_jobs(yaml_file) == [] + assert message in caplog.text + + # --------------------------------------------------------------------------- # Prompt files (prompt_file) # ---------------------------------------------------------------------------