139 lines
5.9 KiB
Python
139 lines
5.9 KiB
Python
"""
|
|||
|
|
Copyright 2024-2026 ReplyPier
|
||
|
|
|
||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
you may not use this file except in compliance with the License.
|
||
|
|
You may obtain a copy of the License at
|
||
|
|
|
||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
|
||
|
|
Unless required by applicable law or agreed to in writing, software
|
||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
See the License for the specific language governing permissions and
|
||
|
|
limitations under the License.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from types import SimpleNamespace
|
||
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.channels.base import ChannelInteraction, InboundMessage
|
||
|
|
from app.models.customer import Customer
|
||
|
|
from app.repositories.channels import (
|
||
|
|
ChannelAccountRepository,
|
||
|
|
ChannelConversationRepository,
|
||
|
|
AgentChannelConfigRepository,
|
||
|
|
)
|
||
|
|
from app.repositories.rating import RatingRepository
|
||
|
|
from app.repositories.session_to_agent import SessionToAgentRepository
|
||
|
|
from app.services import channel_chat
|
||
|
|
from app.services.channel_chat import process_channel_interaction, process_channel_message
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def use_test_db(db, monkeypatch):
|
||
|
|
class _NonClosing:
|
||
|
|
def __init__(self, inner):
|
||
|
|
self._inner = inner
|
||
|
|
def __getattr__(self, name):
|
||
|
|
return getattr(self._inner, name)
|
||
|
|
def close(self):
|
||
|
|
pass
|
||
|
|
monkeypatch.setattr(channel_chat, "SessionLocal", lambda: _NonClosing(db))
|
||
|
|
return db
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def account(db, test_organization):
|
||
|
|
return ChannelAccountRepository(db).create_account(
|
||
|
|
organization_id=test_organization.id,
|
||
|
|
channel_type="telegram",
|
||
|
|
external_account_id="bot1",
|
||
|
|
credentials={"bot_token": "1:x"},
|
||
|
|
display_name="@bot",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def conversation(db, account, test_agent, test_customer, test_organization):
|
||
|
|
import uuid
|
||
|
|
session = SessionToAgentRepository(db).create_session(
|
||
|
|
session_id=uuid.uuid4(),
|
||
|
|
agent_id=test_agent.id, customer_id=test_customer.id,
|
||
|
|
organization_id=test_organization.id, channel="telegram")
|
||
|
|
return ChannelConversationRepository(db).create(
|
||
|
|
channel_account_id=account.id, channel_type="telegram",
|
||
|
|
external_conversation_id="555", external_user_id="555",
|
||
|
|
session_id=session.session_id, organization_id=test_organization.id,
|
||
|
|
agent_id=test_agent.id, customer_id=test_customer.id)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def fake_adapter(monkeypatch):
|
||
|
|
adapter = MagicMock()
|
||
|
|
adapter.send_text = AsyncMock()
|
||
|
|
adapter.send_typing = AsyncMock()
|
||
|
|
adapter.request_phone = AsyncMock()
|
||
|
|
monkeypatch.setattr(channel_chat, "get_adapter", lambda ct: adapter)
|
||
|
|
return adapter
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_contact_stores_phone(db, account, conversation, use_test_db, fake_adapter):
|
||
|
|
interaction = ChannelInteraction(type="contact", external_account_id="",
|
||
|
|
external_conversation_id="555", external_user_id="555",
|
||
|
|
phone="+441234567890")
|
||
|
|
await process_channel_interaction(account.id, interaction)
|
||
|
|
|
||
|
|
customer = db.query(Customer).filter(Customer.id == conversation.customer_id).one()
|
||
|
|
assert customer.meta_data.get("phone") == "+441234567890"
|
||
|
|
# Promoted to the identity column too, so the person is phone-addressable
|
||
|
|
assert customer.phone == "+441234567890"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_typing_shown_no_rating_prompt_on_end_chat(db, account, conversation, use_test_db,
|
||
|
|
fake_adapter, test_ai_config):
|
||
|
|
"""Typing indicator fires, but rating is NEVER asked on a channel — rating
|
||
|
|
is a web-widget-only feature."""
|
||
|
|
AgentChannelConfigRepository(db).set_agent(account.id, conversation.agent_id)
|
||
|
|
response = SimpleNamespace(message="Bye!", transfer_to_human=False, end_chat=True,
|
||
|
|
request_rating=True, request_contact=False,
|
||
|
|
request_lead_capture=False)
|
||
|
|
fake = MagicMock()
|
||
|
|
fake.get_response = AsyncMock(return_value=response)
|
||
|
|
fake.safe_cleanup_mcp_tools = AsyncMock()
|
||
|
|
|
||
|
|
inbound = InboundMessage(external_account_id="", external_conversation_id="555",
|
||
|
|
external_user_id="555", external_message_id="m3", text="bye")
|
||
|
|
with patch.object(channel_chat.ChatAgent, "create_async", AsyncMock(return_value=fake)), \
|
||
|
|
patch.object(channel_chat, "deliver_to_customer", AsyncMock()):
|
||
|
|
await process_channel_message(account.id, inbound)
|
||
|
|
|
||
|
|
fake_adapter.send_typing.assert_awaited()
|
||
|
|
# No rating was recorded and the adapter was never asked to prompt for one
|
||
|
|
assert RatingRepository(db).get_rating_by_session(conversation.session_id) is None
|
||
|
|
assert not hasattr(fake_adapter, "send_rating_prompt") or \
|
||
|
|
not fake_adapter.send_rating_prompt.called
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_request_contact_triggers_phone_prompt(db, account, conversation, use_test_db,
|
||
|
|
fake_adapter, test_ai_config):
|
||
|
|
AgentChannelConfigRepository(db).set_agent(account.id, conversation.agent_id)
|
||
|
|
response = SimpleNamespace(message="Let me connect you.", transfer_to_human=False,
|
||
|
|
end_chat=False, request_rating=False, request_contact=True,
|
||
|
|
request_lead_capture=False)
|
||
|
|
fake = MagicMock()
|
||
|
|
fake.get_response = AsyncMock(return_value=response)
|
||
|
|
fake.safe_cleanup_mcp_tools = AsyncMock()
|
||
|
|
|
||
|
|
inbound = InboundMessage(external_account_id="", external_conversation_id="555",
|
||
|
|
external_user_id="555", external_message_id="m4", text="help")
|
||
|
|
with patch.object(channel_chat.ChatAgent, "create_async", AsyncMock(return_value=fake)), \
|
||
|
|
patch.object(channel_chat, "deliver_to_customer", AsyncMock()):
|
||
|
|
await process_channel_message(account.id, inbound)
|
||
|
|
|
||
|
|
fake_adapter.request_phone.assert_awaited()
|