CliniqAl / tests /test_appointment /test_happy_path.py
Drumil
feat(person-b): implement Patient Flow — appointments, discharge, clerical
66e0039
Raw
History Blame Contribute Delete
5.37 kB
"""Appointment Agent — happy path tests. OWNER: Person B."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
import pytest
from app.contracts import BookingStatus, Channel
# ---------------------------------------------------------------------------
# Mocks
# ---------------------------------------------------------------------------
@pytest.fixture()
def mock_calendar():
"""Patch calendar_mcp so tests don't need a real Google Calendar."""
with patch("app.agents.appointment.api.calendar_mcp") as m:
m.list_events.return_value = [] # empty calendar = all slots free
m.create_event.return_value = {"id": "gcal-test-001"}
yield m
@pytest.fixture()
def mock_celery():
"""Patch Celery send_reminder so tests don't need a running broker."""
with patch("app.agents.appointment.api.send_reminder") as m:
m.apply_async.return_value = MagicMock()
yield m
@pytest.fixture()
def now_window():
now = datetime.now(timezone.utc)
return now + timedelta(days=1, hours=9), now + timedelta(days=1, hours=17)
# ---------------------------------------------------------------------------
# book()
# ---------------------------------------------------------------------------
def test_book_returns_booking_result(mock_calendar, mock_celery, now_window):
from app.agents.appointment.api import book
result = book(patient_id="p-001", requested_window=now_window)
assert result.patient_id == "p-001"
assert result.status == BookingStatus.CONFIRMED
assert result.appointment_id.startswith("appt-")
assert result.calendar_event_id == "gcal-test-001"
mock_calendar.create_event.assert_called_once()
def test_book_enqueues_reminders(mock_calendar, mock_celery, now_window):
from app.agents.appointment.api import book
result = book(patient_id="p-001", requested_window=now_window)
# Should have 2 reminder jobs: T-24h email and T-2h WhatsApp
assert len(result.reminder_jobs) == 2
channels = {j.channel for j in result.reminder_jobs}
assert Channel.EMAIL in channels
assert Channel.WHATSAPP in channels
for job in result.reminder_jobs:
assert job.enqueued is True
def test_book_no_slot_raises(mock_calendar, mock_celery):
"""When the calendar is fully booked, book() should raise ValueError."""
from app.agents.appointment.api import book, _SLOT_DURATION
now = datetime.now(timezone.utc)
w_start = now + timedelta(hours=1)
w_end = w_start + timedelta(minutes=_SLOT_DURATION - 1) # window too narrow
# Make every possible slot busy
mock_calendar.list_events.return_value = [
{
"start_iso": w_start.isoformat(),
"end_iso": (w_start + timedelta(hours=8)).isoformat(),
}
]
with pytest.raises(ValueError, match="No available slot"):
book(patient_id="p-001", requested_window=(w_start, w_end))
def test_book_skips_busy_slots(mock_calendar, mock_celery):
"""book() should skip over occupied slots and find the first free one."""
from app.agents.appointment.api import book, _SLOT_DURATION
now = datetime.now(timezone.utc)
w_start = now + timedelta(days=1, hours=9)
w_end = now + timedelta(days=1, hours=11)
# Block the first slot
mock_calendar.list_events.return_value = [
{
"start_iso": w_start.isoformat(),
"end_iso": (w_start + timedelta(minutes=_SLOT_DURATION)).isoformat(),
}
]
result = book(patient_id="p-002", requested_window=(w_start, w_end))
assert result.slot_iso >= w_start + timedelta(minutes=_SLOT_DURATION)
# ---------------------------------------------------------------------------
# handle_inbound_message()
# ---------------------------------------------------------------------------
def test_handle_inbound_book_intent(mock_calendar, mock_celery):
from app.agents.appointment.api import handle_inbound_message
with patch("app.agents.appointment.api._parse_intent") as mock_parse:
mock_parse.return_value = {"intent": "book", "preferred_date": None}
draft = handle_inbound_message(
channel="whatsapp",
payload={"from": "+919876543210", "body": "book appointment"},
)
assert draft.channel == Channel.WHATSAPP
assert draft.to == "+919876543210"
assert "confirmed" in draft.body.lower() or "appt-" in draft.body
def test_handle_inbound_other_intent():
from app.agents.appointment.api import handle_inbound_message
with patch("app.agents.appointment.api._parse_intent") as mock_parse:
mock_parse.return_value = {"intent": "other"}
draft = handle_inbound_message(
channel="web",
payload={"from": "demo", "body": "hello"},
)
assert "book" in draft.body.lower() or "appointment" in draft.body.lower()
# ---------------------------------------------------------------------------
# cancel()
# ---------------------------------------------------------------------------
def test_cancel_returns_cancelled_result():
from app.agents.appointment.api import cancel
result = cancel("appt-abc123", reason="patient no longer needed")
assert result.status == BookingStatus.CANCELLED
assert result.appointment_id == "appt-abc123"