fix(triggers): coerce null ms fields when loading local triggers
Explicit JSON null for runAtMs/createdAtMs raised TypeError and could quarantine triggers.json. Treat null/blank like a missing key (0).
This commit is contained in:
@@ -11,6 +11,13 @@ from nanobot.utils.dict_keys import get_camel_snake as _get
|
|||||||
TriggerStatus = Literal["ok", "error"]
|
TriggerStatus = Literal["ok", "error"]
|
||||||
|
|
||||||
|
|
||||||
|
def _ms_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
|
@dataclass
|
||||||
class TriggerRunRecord:
|
class TriggerRunRecord:
|
||||||
"""A single local trigger delivery record."""
|
"""A single local trigger delivery record."""
|
||||||
@@ -22,7 +29,7 @@ class TriggerRunRecord:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "TriggerRunRecord":
|
def from_dict(cls, data: dict[str, Any]) -> "TriggerRunRecord":
|
||||||
return cls(
|
return cls(
|
||||||
run_at_ms=int(_get(data, "runAtMs", "run_at_ms", 0)),
|
run_at_ms=_ms_int(_get(data, "runAtMs", "run_at_ms", 0)),
|
||||||
status=str(data.get("status") or "error"), # type: ignore[arg-type]
|
status=str(data.get("status") or "error"), # type: ignore[arg-type]
|
||||||
error=data.get("error"),
|
error=data.get("error"),
|
||||||
)
|
)
|
||||||
@@ -70,8 +77,8 @@ class LocalTrigger:
|
|||||||
session_key=str(_get(data, "sessionKey", "session_key", "")),
|
session_key=str(_get(data, "sessionKey", "session_key", "")),
|
||||||
sender_id=str(_get(data, "senderId", "sender_id", "trigger") or "trigger"),
|
sender_id=str(_get(data, "senderId", "sender_id", "trigger") or "trigger"),
|
||||||
origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}),
|
origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}),
|
||||||
created_at_ms=int(_get(data, "createdAtMs", "created_at_ms", 0)),
|
created_at_ms=_ms_int(_get(data, "createdAtMs", "created_at_ms", 0)),
|
||||||
updated_at_ms=int(_get(data, "updatedAtMs", "updated_at_ms", 0)),
|
updated_at_ms=_ms_int(_get(data, "updatedAtMs", "updated_at_ms", 0)),
|
||||||
last_run_at_ms=_get(data, "lastRunAtMs", "last_run_at_ms"),
|
last_run_at_ms=_get(data, "lastRunAtMs", "last_run_at_ms"),
|
||||||
last_status=_get(data, "lastStatus", "last_status"), # type: ignore[arg-type]
|
last_status=_get(data, "lastStatus", "last_status"), # type: ignore[arg-type]
|
||||||
last_error=_get(data, "lastError", "last_error"),
|
last_error=_get(data, "lastError", "last_error"),
|
||||||
@@ -120,8 +127,8 @@ class TriggerDelivery:
|
|||||||
id=str(data["id"]),
|
id=str(data["id"]),
|
||||||
trigger_id=str(_get(data, "triggerId", "trigger_id", "")),
|
trigger_id=str(_get(data, "triggerId", "trigger_id", "")),
|
||||||
content=str(data.get("content") or ""),
|
content=str(data.get("content") or ""),
|
||||||
created_at_ms=int(_get(data, "createdAtMs", "created_at_ms", 0)),
|
created_at_ms=_ms_int(_get(data, "createdAtMs", "created_at_ms", 0)),
|
||||||
attempts=int(data.get("attempts", 0)),
|
attempts=_ms_int(data.get("attempts", 0)),
|
||||||
last_error=data.get("lastError") or data.get("last_error"),
|
last_error=data.get("lastError") or data.get("last_error"),
|
||||||
path=path,
|
path=path,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -490,3 +490,29 @@ async def test_local_trigger_queue_recovers_processing_delivery_on_start(
|
|||||||
assert submitted[0].content == "Review PR #4591"
|
assert submitted[0].content == "Review PR #4591"
|
||||||
assert submitted[0].metadata["_local_trigger"]["trigger_id"] == trigger.id
|
assert submitted[0].metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||||
assert restarted.claim_deliveries() == []
|
assert restarted.claim_deliveries() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_trigger_from_dict_accepts_null_run_at_ms() -> None:
|
||||||
|
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery
|
||||||
|
|
||||||
|
trigger = LocalTrigger.from_dict(
|
||||||
|
{
|
||||||
|
"id": "t1",
|
||||||
|
"name": "n",
|
||||||
|
"enabled": True,
|
||||||
|
"channel": "websocket",
|
||||||
|
"chatId": "c1",
|
||||||
|
"sessionKey": "websocket:c1",
|
||||||
|
"runHistory": [{"runAtMs": None, "status": "ok"}],
|
||||||
|
"createdAtMs": None,
|
||||||
|
"updatedAtMs": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert trigger.run_history[0].run_at_ms == 0
|
||||||
|
assert trigger.created_at_ms == 0
|
||||||
|
assert trigger.updated_at_ms == 0
|
||||||
|
|
||||||
|
delivery = TriggerDelivery.from_dict(
|
||||||
|
{"id": "d1", "triggerId": "t1", "content": "hi", "createdAtMs": None}
|
||||||
|
)
|
||||||
|
assert delivery.created_at_ms == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user