From b0d234f068952e7bc198759ae3ca2cda99bae491 Mon Sep 17 00:00:00 2001 From: Acean <27935764+liuboacean@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:13:11 -0700 Subject: [PATCH] fix(cron): don't crash on `cron list` when a job's repeat is null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cron_list` read `job.get("repeat", {})`, but the dict-default only applies to a MISSING key. A one-shot job persisted with `"repeat": null` returns None, and the next `.get("times")` raised AttributeError, taking down the whole `cron list` output. Coalesce with `or {}` so a present-but-null repeat renders as ∞ like the other cron readers already do. Adds a regression test. Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> --- hermes_cli/cron.py | 5 ++++- tests/hermes_cli/test_cron.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/hermes_cli/cron.py b/hermes_cli/cron.py index fa0acd819..683fc73fb 100644 --- a/hermes_cli/cron.py +++ b/hermes_cli/cron.py @@ -81,7 +81,10 @@ def cron_list(show_all: bool = False): state = job.get("state", "scheduled" if job.get("enabled", True) else "paused") next_run = job.get("next_run_at", "?") - repeat_info = job.get("repeat", {}) + # `repeat` may be present-but-null in the job record (e.g. a one-shot + # job persisted with "repeat": null), so coalesce to {} rather than + # relying on the dict-default, which only applies to a missing key. + repeat_info = job.get("repeat") or {} repeat_times = repeat_info.get("times") repeat_completed = repeat_info.get("completed", 0) repeat_str = f"{repeat_completed}/{repeat_times}" if repeat_times else "∞" diff --git a/tests/hermes_cli/test_cron.py b/tests/hermes_cli/test_cron.py index 49628f1a4..aa4f6b116 100644 --- a/tests/hermes_cli/test_cron.py +++ b/tests/hermes_cli/test_cron.py @@ -111,3 +111,19 @@ class TestCronCommandLifecycle: assert jobs[0]["skills"] == ["blogwatcher", "maps"] assert jobs[0]["name"] == "Skill combo" assert jobs[0]["profile"] == "default" + + def test_list_does_not_crash_when_repeat_is_null(self, tmp_cron_dir, capsys): + """A one-shot job can be persisted with ``"repeat": null``. `cron + list` must render it as ∞ rather than crashing on .get(...)\\.get.""" + from cron.jobs import load_jobs, save_jobs + + create_job(prompt="One shot", schedule="every 1h") + # Force the present-but-null shape that .get("repeat", {}) mishandles. + jobs = load_jobs() + jobs[0]["repeat"] = None + save_jobs(jobs) + + cron_command(Namespace(cron_command="list", all=True)) + + out = capsys.readouterr().out + assert "Repeat: ∞" in out