fix(cron): coerce string schedule/state ms fields from jobs.json
jobs.json can store everyMs/atMs and next/last run timestamps as strings. Loading left them as str, so _compute_next_run compared str to int and raised TypeError. Coerce with an optional-int helper at from_store_dict, matching runHistory int() paths.
This commit is contained in:
@@ -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 _store_int(value: Any, default: int = 0) -> int:
|
def _store_int(value: Any, default: int | None = 0) -> int | None:
|
||||||
"""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
|
||||||
@@ -32,8 +32,8 @@ class CronSchedule:
|
|||||||
def from_store_dict(cls, data: dict[str, Any]) -> CronSchedule:
|
def from_store_dict(cls, data: dict[str, Any]) -> CronSchedule:
|
||||||
return cls(
|
return cls(
|
||||||
kind=data["kind"],
|
kind=data["kind"],
|
||||||
at_ms=get_camel_snake(data, "atMs", "at_ms"),
|
at_ms=_store_int(get_camel_snake(data, "atMs", "at_ms"), None),
|
||||||
every_ms=get_camel_snake(data, "everyMs", "every_ms"),
|
every_ms=_store_int(get_camel_snake(data, "everyMs", "every_ms"), None),
|
||||||
expr=data.get("expr"),
|
expr=data.get("expr"),
|
||||||
tz=data.get("tz"),
|
tz=data.get("tz"),
|
||||||
)
|
)
|
||||||
@@ -105,8 +105,12 @@ class CronJobState:
|
|||||||
def from_store_dict(cls, data: dict[str, Any]) -> CronJobState:
|
def from_store_dict(cls, data: dict[str, Any]) -> CronJobState:
|
||||||
history = get_camel_snake(data, "runHistory", "run_history", []) or []
|
history = get_camel_snake(data, "runHistory", "run_history", []) or []
|
||||||
return cls(
|
return cls(
|
||||||
next_run_at_ms=get_camel_snake(data, "nextRunAtMs", "next_run_at_ms"),
|
next_run_at_ms=_store_int(
|
||||||
last_run_at_ms=get_camel_snake(data, "lastRunAtMs", "last_run_at_ms"),
|
get_camel_snake(data, "nextRunAtMs", "next_run_at_ms"), None
|
||||||
|
),
|
||||||
|
last_run_at_ms=_store_int(
|
||||||
|
get_camel_snake(data, "lastRunAtMs", "last_run_at_ms"), None
|
||||||
|
),
|
||||||
last_status=get_camel_snake(data, "lastStatus", "last_status"),
|
last_status=get_camel_snake(data, "lastStatus", "last_status"),
|
||||||
last_error=get_camel_snake(data, "lastError", "last_error"),
|
last_error=get_camel_snake(data, "lastError", "last_error"),
|
||||||
run_history=[
|
run_history=[
|
||||||
|
|||||||
@@ -65,6 +65,44 @@ def test_load_jobs_accepts_snake_case_schedule_and_run_history(tmp_path) -> None
|
|||||||
assert jobs[0].state.run_history[0].duration_ms == 12
|
assert jobs[0].state.run_history[0].duration_ms == 12
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_jobs_coerces_string_schedule_and_state_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": "60000"},
|
||||||
|
"payload": {
|
||||||
|
"kind": "agent_turn",
|
||||||
|
"message": "hi",
|
||||||
|
"sessionKey": "websocket:chat-1",
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"nextRunAtMs": "100",
|
||||||
|
"lastRunAtMs": "50",
|
||||||
|
},
|
||||||
|
"createdAtMs": 0,
|
||||||
|
"updatedAtMs": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
jobs, _version = CronService(store_path)._load_jobs()
|
||||||
|
assert jobs is not None
|
||||||
|
assert jobs[0].schedule.every_ms == 60_000
|
||||||
|
assert jobs[0].state.next_run_at_ms == 100
|
||||||
|
assert jobs[0].state.last_run_at_ms == 50
|
||||||
|
|
||||||
|
|
||||||
def test_add_job_rejects_unknown_timezone(tmp_path) -> None:
|
def test_add_job_rejects_unknown_timezone(tmp_path) -> None:
|
||||||
service = CronService(tmp_path / "cron" / "jobs.json")
|
service = CronService(tmp_path / "cron" / "jobs.json")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user