fix(cron): also coerce null createdAtMs/updatedAtMs on load

Same present-null footgun as runHistory; route all required store
ints through _store_int.
This commit is contained in:
santhreal 2026-07-18 17:24:46 -07:00 committed by Xubin Ren
parent 0b1b02f187
commit b6156fdd79
2 changed files with 9 additions and 9 deletions

View File

@ -8,7 +8,7 @@ from typing import Any, Literal
from nanobot.utils.dict_keys import get_camel_snake from nanobot.utils.dict_keys import get_camel_snake
def _required_store_int(value: Any, default: int = 0) -> int: def _store_int(value: Any, default: int = 0) -> int:
"""Coerce JSON numerics to int; treat null/blank like a missing key.""" """Coerce JSON numerics to int; treat null/blank like a missing key."""
if value is None or value == "": if value is None or value == "":
return default return default
@ -85,11 +85,9 @@ class CronRunRecord:
@classmethod @classmethod
def from_store_dict(cls, data: dict[str, Any]) -> CronRunRecord: def from_store_dict(cls, data: dict[str, Any]) -> CronRunRecord:
return cls( return cls(
run_at_ms=_required_store_int(get_camel_snake(data, "runAtMs", "run_at_ms", 0)), run_at_ms=_store_int(get_camel_snake(data, "runAtMs", "run_at_ms", 0)),
status=data["status"], status=data["status"],
duration_ms=_required_store_int( duration_ms=_store_int(get_camel_snake(data, "durationMs", "duration_ms", 0)),
get_camel_snake(data, "durationMs", "duration_ms", 0)
),
error=data.get("error"), error=data.get("error"),
) )
@ -155,8 +153,8 @@ class CronJob:
schedule=CronSchedule.from_store_dict(data["schedule"]), schedule=CronSchedule.from_store_dict(data["schedule"]),
payload=CronPayload.from_store_dict(data.get("payload") or {}), payload=CronPayload.from_store_dict(data.get("payload") or {}),
state=CronJobState.from_store_dict(data.get("state") or {}), state=CronJobState.from_store_dict(data.get("state") or {}),
created_at_ms=int(get_camel_snake(data, "createdAtMs", "created_at_ms", 0)), created_at_ms=_store_int(get_camel_snake(data, "createdAtMs", "created_at_ms", 0)),
updated_at_ms=int(get_camel_snake(data, "updatedAtMs", "updated_at_ms", 0)), updated_at_ms=_store_int(get_camel_snake(data, "updatedAtMs", "updated_at_ms", 0)),
delete_after_run=bool( delete_after_run=bool(
get_camel_snake(data, "deleteAfterRun", "delete_after_run", False) get_camel_snake(data, "deleteAfterRun", "delete_after_run", False)
), ),

View File

@ -1032,8 +1032,8 @@ def test_load_jobs_accepts_null_run_history_ms(tmp_path) -> None:
{"runAtMs": None, "status": "ok", "durationMs": None}, {"runAtMs": None, "status": "ok", "durationMs": None},
], ],
}, },
"createdAtMs": 0, "createdAtMs": None,
"updatedAtMs": 0, "updatedAtMs": None,
} }
], ],
} }
@ -1046,3 +1046,5 @@ def test_load_jobs_accepts_null_run_history_ms(tmp_path) -> None:
assert jobs[0].state.run_history[0].run_at_ms == 0 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].duration_ms == 0
assert jobs[0].state.run_history[0].status == "ok" assert jobs[0].state.run_history[0].status == "ok"
assert jobs[0].created_at_ms == 0
assert jobs[0].updated_at_ms == 0