diff --git a/nanobot/triggers/local_types.py b/nanobot/triggers/local_types.py index f7343963..d3fa0a28 100644 --- a/nanobot/triggers/local_types.py +++ b/nanobot/triggers/local_types.py @@ -16,6 +16,13 @@ def _int_or_zero(value: Any) -> int: return 0 if value is None or value == "" else int(value) +def _optional_int(value: Any) -> int | None: + """Coerce a stored JSON numeric; null/blank stays None.""" + if value is None or value == "": + return None + return int(value) + + @dataclass class TriggerRunRecord: """A single local trigger delivery record.""" @@ -77,7 +84,7 @@ class LocalTrigger: origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}), created_at_ms=_int_or_zero(_get(data, "createdAtMs", "created_at_ms", 0)), updated_at_ms=_int_or_zero(_get(data, "updatedAtMs", "updated_at_ms", 0)), - last_run_at_ms=_get(data, "lastRunAtMs", "last_run_at_ms"), + last_run_at_ms=_optional_int(_get(data, "lastRunAtMs", "last_run_at_ms")), last_status=_get(data, "lastStatus", "last_status"), # type: ignore[arg-type] last_error=_get(data, "lastError", "last_error"), run_history=history, diff --git a/tests/triggers/test_local_triggers.py b/tests/triggers/test_local_triggers.py index 41fe91d0..d4fd279f 100644 --- a/tests/triggers/test_local_triggers.py +++ b/tests/triggers/test_local_triggers.py @@ -585,3 +585,35 @@ def test_local_trigger_from_dict_accepts_null_run_at_ms() -> None: ) assert delivery.created_at_ms == 0 assert delivery.attempts == 0 + + +def test_local_trigger_from_dict_coerces_string_last_run_at_ms() -> None: + """String lastRunAtMs must coerce to int like cron store ms fields.""" + trigger = LocalTrigger.from_dict( + { + "id": "t1", + "name": "n", + "enabled": True, + "channel": "websocket", + "chatId": "c1", + "sessionKey": "websocket:c1", + "lastRunAtMs": "1710000000000", + "createdAtMs": 1, + "updatedAtMs": 1, + } + ) + assert trigger.last_run_at_ms == 1710000000000 + assert trigger.last_run_at_ms < 1710000000001 + + trigger_null = LocalTrigger.from_dict( + { + "id": "t2", + "name": "n", + "enabled": True, + "sessionKey": "websocket:c1", + "lastRunAtMs": None, + "createdAtMs": 1, + "updatedAtMs": 1, + } + ) + assert trigger_null.last_run_at_ms is None