Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cueapi/resources/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, Optional
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

if TYPE_CHECKING:
from cueapi.client import CueAPI
Expand Down Expand Up @@ -33,6 +34,7 @@ def send(
reply_to_agent: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
idempotency_key: Optional[str] = None,
send_at: Optional[Union[str, datetime]] = None,
) -> dict:
"""Send a message.

Expand Down Expand Up @@ -69,6 +71,11 @@ def send(
metadata: Optional JSON metadata blob.
idempotency_key: Optional ``Idempotency-Key`` header
(≤255 chars).
send_at: Optional ISO 8601 timestamp (or ``datetime``) to
delay this message's delivery. If omitted, the message
is delivered immediately. Per-message scheduling landed
in cueapi #623 — server stores ``send_at`` on the
message row and the worker picks it up when due.

Returns:
Dict matching the server's ``MessageResponse`` shape.
Expand All @@ -95,6 +102,10 @@ def send(
payload["reply_to_agent"] = reply_to_agent
if metadata is not None:
payload["metadata"] = metadata
if send_at is not None:
payload["send_at"] = (
send_at.isoformat() if isinstance(send_at, datetime) else send_at
)

headers: Dict[str, str] = {"X-Cueapi-From-Agent": from_agent}
if idempotency_key is not None:
Expand Down
61 changes: 61 additions & 0 deletions tests/test_messages_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,64 @@ def test_ack(self):
mock_client._post.assert_called_once_with(
"/v1/messages/msg_x/ack", json={},
)


class TestSendAt:
"""Per-message scheduling via send_at (cueapi #623 parity port).

Mock-based following the existing pattern in this file. Asserts on
the request body shape — that's the SDK contract; what the server
does with it (delay then deliver) is exercised by the server suite.
"""

def test_send_with_send_at_iso_string(self):
"""send_at as ISO 8601 string flows into the request body verbatim."""
mock_client = MagicMock()
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
r = MessagesResource(mock_client)

r.send(
from_agent="sender@x",
to="recipient@y",
body="hi",
send_at="2030-01-01T12:00:00Z",
)

mock_client._post.assert_called_once_with(
"/v1/messages",
json={
"to": "recipient@y",
"body": "hi",
"send_at": "2030-01-01T12:00:00Z",
},
headers={"X-Cueapi-From-Agent": "sender@x"},
)

def test_send_with_send_at_datetime_auto_isoformats(self):
"""send_at as datetime auto-serializes via .isoformat()."""
from datetime import datetime, timezone

mock_client = MagicMock()
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
r = MessagesResource(mock_client)

r.send(
from_agent="sender@x",
to="recipient@y",
body="hi",
send_at=datetime(2030, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
)

call = mock_client._post.call_args
assert call.kwargs["json"]["send_at"] == "2030-01-01T12:00:00+00:00"

def test_send_without_send_at_omits_field(self):
"""send_at unset → field NOT present in request body."""
mock_client = MagicMock()
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
r = MessagesResource(mock_client)

r.send(from_agent="sender@x", to="recipient@y", body="hi")

call = mock_client._post.call_args
assert "send_at" not in call.kwargs["json"]
Loading