With DISPATCH_AGENT autonomous=true in the agent registry, the assignment-failure
path sends a real customer message. It would have sent
"Delay alert: Order 36 may arrive later than expected. New ETA: {new_eta}"
with customer_id "unknown": the DELAYED template names {new_eta} but DispatchAgent
supplied only order_id, and it has no customer id to give.
- _send_via_channel refuses to send when any {placeholder} survives substitution
(or the template is missing), logs which variables the caller omitted, and
records a failed notification. Silence beats a broken message.
- customer_id is omitted from the payload when it is absent or "unknown" rather
than sent literally; the backend resolves the recipient from order_id.
- DispatchAgent supplies new_eta "being confirmed" — honest, since no ETA exists
at assignment-failure time.
- Tests: the old bug is refused, a complete payload renders clean, the id is
omitted/passed correctly, and DispatchAgent's vars satisfy the template.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AJLYcbTHCe45fyFnMfEin
118 lines
3.9 KiB
Python
118 lines
3.9 KiB
Python
"""CUSTOMER_AGENT must never put an unsubstituted {placeholder} in front of a
|
|
customer, and must not send a literal "unknown" customer id.
|
|
|
|
The DispatchAgent assignment-failure path knows only a booking id, so this is
|
|
the realistic caller: it supplies order_id and new_eta and no customer.
|
|
"""
|
|
import pytest
|
|
|
|
from agents.customer_agent import (
|
|
CustomerAgent, NotificationChannel, NotificationType, _UNFILLED_PLACEHOLDER,
|
|
)
|
|
|
|
|
|
def make_agent(captured):
|
|
agent = CustomerAgent.__new__(CustomerAgent)
|
|
agent.agent_id = "CUSTOMER_AGENT"
|
|
agent._notifications = {}
|
|
agent._templates = CustomerAgent._init_templates(agent)
|
|
|
|
async def fake_post(url, json=None, headers=None, timeout=None):
|
|
captured.append(json)
|
|
return {"ok": True}
|
|
|
|
import agents.customer_agent as mod
|
|
mod.api_post = fake_post
|
|
return agent
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def restore_api_post():
|
|
import agents.customer_agent as mod
|
|
original = mod.api_post
|
|
yield
|
|
mod.api_post = original
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_template_var_is_refused_not_sent():
|
|
sent = []
|
|
agent = make_agent(sent)
|
|
result = await agent._send_via_channel(
|
|
order_id="36", customer_id="unknown",
|
|
channel=NotificationChannel.SMS,
|
|
notification_type=NotificationType.DELAYED,
|
|
template_vars={"order_id": "36"}, # new_eta omitted — the old bug
|
|
)
|
|
assert result["status"] == "failed"
|
|
assert "new_eta" in result["reason"]
|
|
assert sent == [] # nothing reached the backend
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_vars_supplied_sends_clean_message():
|
|
sent = []
|
|
agent = make_agent(sent)
|
|
result = await agent._send_via_channel(
|
|
order_id="36", customer_id="unknown",
|
|
channel=NotificationChannel.SMS,
|
|
notification_type=NotificationType.DELAYED,
|
|
template_vars={"order_id": "36", "new_eta": "being confirmed"},
|
|
)
|
|
assert result["status"] == "sent"
|
|
assert len(sent) == 1
|
|
assert not _UNFILLED_PLACEHOLDER.findall(sent[0]["message"])
|
|
assert sent[0]["message"] == (
|
|
"Delay alert: Order 36 may arrive later than expected. New ETA: being confirmed"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_customer_id_is_omitted_from_payload():
|
|
sent = []
|
|
agent = make_agent(sent)
|
|
await agent._send_via_channel(
|
|
order_id="36", customer_id="unknown",
|
|
channel=NotificationChannel.SMS,
|
|
notification_type=NotificationType.DELAYED,
|
|
template_vars={"order_id": "36", "new_eta": "being confirmed"},
|
|
)
|
|
assert "customer_id" not in sent[0] # backend resolves from order_id
|
|
assert sent[0]["order_id"] == "36"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_real_customer_id_is_sent():
|
|
sent = []
|
|
agent = make_agent(sent)
|
|
await agent._send_via_channel(
|
|
order_id="36", customer_id="CUST-9",
|
|
channel=NotificationChannel.SMS,
|
|
notification_type=NotificationType.DELAYED,
|
|
template_vars={"order_id": "36", "new_eta": "being confirmed"},
|
|
)
|
|
assert sent[0]["customer_id"] == "CUST-9"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dispatch_supplies_every_template_var():
|
|
"""The DispatchAgent payload must satisfy the DELAYED template exactly."""
|
|
from agents.dispatch_agent import DispatchAgent
|
|
sent_msgs = []
|
|
|
|
async def capture(recipient, message_type, payload, correlation_id=None):
|
|
sent_msgs.append(payload)
|
|
|
|
agent = DispatchAgent.__new__(DispatchAgent)
|
|
agent.send_message = capture
|
|
await agent._notify_customer_delay("36")
|
|
|
|
payload = sent_msgs[0]
|
|
assert payload["task_type"] == "send_notification"
|
|
template = CustomerAgent._init_templates(CustomerAgent.__new__(CustomerAgent))[
|
|
NotificationType.DELAYED]["sms"]
|
|
required = set(_UNFILLED_PLACEHOLDER.findall(template))
|
|
assert required <= set(payload["template_vars"]), (
|
|
f"template needs {required}, dispatch supplies {set(payload['template_vars'])}"
|
|
)
|