Files
AI_engine/tests/test_customer_notify.py
Suriyakumarvijayanayagam 9cbb6f9704 fix(customer): never send an unsubstituted placeholder or a fake customer id
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
2026-09-30 15:14:08 +05:30

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'])}"
)