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