diff --git a/nanobot/cron/types.py b/nanobot/cron/types.py index 1b615f31..772aee40 100644 --- a/nanobot/cron/types.py +++ b/nanobot/cron/types.py @@ -8,6 +8,13 @@ from typing import Any, Literal from nanobot.utils.dict_keys import get_camel_snake +def _required_store_int(value: Any, default: int = 0) -> int: + """Coerce JSON numerics to int; treat null/blank like a missing key.""" + if value is None or value == "": + return default + return int(value) + + @dataclass class CronSchedule: """Schedule definition for a cron job.""" @@ -78,9 +85,11 @@ class CronRunRecord: @classmethod def from_store_dict(cls, data: dict[str, Any]) -> CronRunRecord: return cls( - run_at_ms=int(get_camel_snake(data, "runAtMs", "run_at_ms", 0)), + run_at_ms=_required_store_int(get_camel_snake(data, "runAtMs", "run_at_ms", 0)), status=data["status"], - duration_ms=int(get_camel_snake(data, "durationMs", "duration_ms", 0)), + duration_ms=_required_store_int( + get_camel_snake(data, "durationMs", "duration_ms", 0) + ), error=data.get("error"), ) diff --git a/tests/cron/test_cron_service.py b/tests/cron/test_cron_service.py index 5534c8d0..83a605e3 100644 --- a/tests/cron/test_cron_service.py +++ b/tests/cron/test_cron_service.py @@ -1007,3 +1007,42 @@ async def test_list_jobs_during_on_job_does_not_cause_stale_reload(tmp_path) -> next_run = j["state"]["nextRunAtMs"] assert next_run is not None assert next_run > now_ms, f"Job '{j['name']}' next_run should be in the future" + + +def test_load_jobs_accepts_null_run_history_ms(tmp_path) -> None: + store_path = tmp_path / "cron" / "jobs.json" + store_path.parent.mkdir(parents=True) + store_path.write_text( + json.dumps( + { + "version": 1, + "jobs": [ + { + "id": "j1", + "name": "t", + "enabled": True, + "schedule": {"kind": "every", "everyMs": 60_000}, + "payload": { + "kind": "agent_turn", + "message": "hi", + "sessionKey": "websocket:chat-1", + }, + "state": { + "runHistory": [ + {"runAtMs": None, "status": "ok", "durationMs": None}, + ], + }, + "createdAtMs": 0, + "updatedAtMs": 0, + } + ], + } + ), + encoding="utf-8", + ) + + jobs, _version = CronService(store_path)._load_jobs() + assert jobs is not None + assert jobs[0].state.run_history[0].run_at_ms == 0 + assert jobs[0].state.run_history[0].duration_ms == 0 + assert jobs[0].state.run_history[0].status == "ok"