""" 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. """ import pytest from fastapi.testclient import TestClient from app.database import get_db from fastapi import FastAPI, status from app.models.user import User from app.models.role import Role from app.models.permission import Permission, role_permissions from app.models.session_to_agent import SessionToAgent, SessionStatus from app.models.chat_history import ChatHistory from app.models.agent import Agent, AgentType from app.models.customer import Customer from uuid import uuid4 from app.api import session_to_agent as session_to_agent_router from app.core.auth import get_current_user from typing import Generator from datetime import datetime from app.models.schemas.chat import ChatDetailResponse, CustomerInfo, AgentInfo, Message from tests.conftest import engine, TestingSessionLocal, create_tables, Base from app.models.organization import Organization from app.repositories.session_to_agent import SessionToAgentRepository from unittest.mock import patch # Create a test FastAPI app app = FastAPI() app.include_router( session_to_agent_router.router, prefix="/api/v1/session-to-agent", tags=["session-to-agent"] ) @pytest.fixture(scope="function") def db() -> Generator: """Create a fresh database for each test.""" # Drop all tables first Base.metadata.drop_all(bind=engine) # Create tables except enterprise ones create_tables() db = TestingSessionLocal() try: yield db finally: db.close() Base.metadata.drop_all(bind=engine) @pytest.fixture def test_permissions(db) -> list[Permission]: """Create test permissions""" permissions = [] # manage_all_chats, not "manage_chats": the latter was never in # Permission.default_permissions(), so the endpoint check it fed never # matched anyone. view_unassigned_chats is what lets an agent see (and # therefore claim) a chat the AI is still handling. for name in ["manage_all_chats", "manage_assigned_chats", "view_unassigned_chats"]: perm = Permission( name=name, description=f"Test permission for {name}" ) db.add(perm) permissions.append(perm) db.commit() for p in permissions: db.refresh(p) return permissions @pytest.fixture def test_role_with_manage_chats(db, test_permissions) -> Role: """Create a test role with manage_all_chats permission""" role = Role( id=1, name="Manage Chats Role", description="Role with manage_all_chats permission", is_default=False ) db.add(role) db.commit() # Add manage_all_chats permission manage_chats_perm = next(p for p in test_permissions if p.name == "manage_all_chats") db.execute( role_permissions.insert().values( role_id=role.id, permission_id=manage_chats_perm.id ) ) db.commit() db.refresh(role) return role @pytest.fixture def test_role_with_manage_assigned_chats(db, test_permissions) -> Role: """Create a test role with manage_assigned_chats permission""" role = Role( id=2, name="Manage Assigned Chats Role", description="Role with manage_assigned_chats permission", is_default=False ) db.add(role) db.commit() # Add manage_assigned_chats + view_unassigned_chats, mirroring the seeded # Agent role: claiming an unclaimed chat requires being able to see it. for perm_name in ("manage_assigned_chats", "view_unassigned_chats"): perm = next(p for p in test_permissions if p.name == perm_name) db.execute( role_permissions.insert().values( role_id=role.id, permission_id=perm.id ) ) db.commit() db.refresh(role) return role @pytest.fixture def test_role_without_permissions(db) -> Role: """Create a test role without any permissions""" role = Role( id=3, name="No Permissions Role", description="Role without permissions", is_default=False ) db.add(role) db.commit() db.refresh(role) return role @pytest.fixture def test_organization(db) -> Organization: """Create a test organization""" organization = Organization( id=uuid4(), name="Test Organization", domain="test.example.com", business_hours={"monday": {"start": "09:00", "end": "17:00"}}, settings={"timezone": "UTC"} ) db.add(organization) db.commit() db.refresh(organization) return organization @pytest.fixture def user_with_manage_chats_permission(db, test_role_with_manage_chats, test_organization) -> User: """Create a test user with manage_chats permission""" user = User( id=uuid4(), email="manage_chats@example.com", hashed_password="hashed_password", is_active=True, organization_id=test_organization.id, full_name="Manage Chats User", role_id=test_role_with_manage_chats.id ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture def user_with_manage_assigned_chats(db, test_role_with_manage_assigned_chats, test_organization, test_agent) -> User: """Create a test user with manage_assigned_chats permission""" user = User( id=uuid4(), email="manage_assigned@example.com", hashed_password="hashed_password", is_active=True, organization_id=test_organization.id, full_name="Manage Assigned Chats User", role_id=test_role_with_manage_assigned_chats.id ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture def regular_user(db, test_role_without_permissions, test_organization) -> User: """Create a test user without special permissions""" user = User( id=uuid4(), email="regular@example.com", hashed_password="hashed_password", is_active=True, organization_id=test_organization.id, full_name="Regular User", role_id=test_role_without_permissions.id ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture def test_agent(db, test_organization) -> Agent: """Create a test agent""" agent = Agent( id=uuid4(), name="Test Agent", display_name="Test Display Name", agent_type=AgentType.CUSTOMER_SUPPORT, instructions=["Test instruction"], is_active=True, organization_id=test_organization.id ) db.add(agent) db.commit() db.refresh(agent) return agent @pytest.fixture def test_customer(db, test_organization) -> Customer: """Create a test customer""" customer = Customer( id=uuid4(), organization_id=test_organization.id, email="customer@example.com", full_name="Test Customer" ) db.add(customer) db.commit() db.refresh(customer) return customer @pytest.fixture def create_chat_session(db, test_agent, test_customer): """Create a test chat session""" def _create_session(): session = SessionToAgent( session_id=uuid4(), organization_id=test_agent.organization_id, customer_id=test_customer.id, agent_id=test_agent.id ) db.add(session) db.commit() db.refresh(session) # Create a test message for the session message = ChatHistory( message="Test message", message_type="agent", session_id=session.session_id, organization_id=session.organization_id, agent_id=session.agent_id, customer_id=session.customer_id ) db.add(message) db.commit() return session return _create_session @pytest.fixture def mock_chat_response(test_agent, test_customer) -> ChatDetailResponse: """Create a mock chat response""" return ChatDetailResponse( customer=CustomerInfo( id=test_customer.id, email=test_customer.email, full_name=test_customer.full_name ), agent=AgentInfo( id=test_agent.id, name=test_agent.name, display_name=test_agent.display_name ), messages=[ Message( message="Test message", message_type="agent", created_at=datetime.now(), attributes={} ) ], status=SessionStatus.OPEN, group_id=None, session_id=uuid4(), user_id=None, user_name=None, created_at=datetime.now(), updated_at=datetime.now() ) @pytest.fixture def client(user_with_manage_chats_permission) -> TestClient: """Create test client with mocked dependencies. The takeover endpoint itself is the real one: these tests assert on the status codes it raises (403/404/400/500), so it must not be swapped for a stub. Do not rebuild the sub-router's routes here — the app copied them at include time, and on current FastAPI a late swap would replace the endpoint under test. """ async def override_get_current_user(): return user_with_manage_chats_permission app.dependency_overrides[get_current_user] = override_get_current_user app.dependency_overrides[get_db] = lambda: TestingSessionLocal() return TestClient(app) @pytest.fixture def client_with_error_mock(user_with_manage_chats_permission, mock_chat_response) -> TestClient: """Create test client with error mocking capabilities""" async def override_get_current_user(): return user_with_manage_chats_permission app.dependency_overrides[get_current_user] = override_get_current_user app.dependency_overrides[get_db] = lambda: TestingSessionLocal() return TestClient(app) def test_takeover_chat_success(client, db, user_with_manage_chats_permission, create_chat_session, mock_chat_response): """Test successful chat takeover""" # Create test data session = create_chat_session() # Test takeover with direct auth response = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) print("Response:", response.json()) # Add this line to print the error message assert response.status_code == status.HTTP_200_OK data = response.json() assert isinstance(data, dict) assert "customer" in data assert "agent" in data assert "messages" in data assert "status" in data assert data["status"] == SessionStatus.OPEN.value def test_takeover_chat_no_permission(client, db, regular_user, create_chat_session): """Test chat takeover with insufficient permissions""" session = create_chat_session() # Override get_current_user to return regular_user async def override_get_current_user(): return regular_user app.dependency_overrides[get_current_user] = override_get_current_user # Test takeover response = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_403_FORBIDDEN assert "Not enough permissions" in response.json()["detail"] def test_takeover_chat_invalid_session(client, db, user_with_manage_chats_permission): """Test chat takeover with invalid session ID""" # Test takeover with invalid session ID invalid_session_id = str(uuid4()) response = client.post( f"/api/v1/session-to-agent/{invalid_session_id}/takeover" ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "Chat session not found" in response.json()["detail"] def test_takeover_chat_unauthorized(client, db, create_chat_session): """Test chat takeover without authentication""" session = create_chat_session() # Remove all dependency overrides app.dependency_overrides = {} # Test takeover without auth response = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_takeover_chat_with_manage_assigned_chats(client, db, user_with_manage_assigned_chats, create_chat_session, mock_chat_response): """Test chat takeover with manage_assigned_chats permission""" session = create_chat_session() # Override get_current_user to return user with manage_assigned_chats async def override_get_current_user(): return user_with_manage_assigned_chats app.dependency_overrides[get_current_user] = override_get_current_user # Test takeover response = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) print("Response:", response.json()) # Add this line to print the error message assert response.status_code == status.HTTP_200_OK data = response.json() assert isinstance(data, dict) assert "customer" in data assert "agent" in data assert "messages" in data assert "status" in data assert data["status"] == SessionStatus.OPEN.value def test_takeover_chat_update_failure(client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session): """Test chat takeover when session update fails""" session = create_chat_session() # Mock the repository to simulate update failure def mock_takeover_session(*args, **kwargs): return False with patch('app.repositories.session_to_agent.SessionToAgentRepository.takeover_session', side_effect=mock_takeover_session): response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Failed to take over chat" in response.json()["detail"] def test_takeover_chat_details_failure(client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session): """Test chat takeover when getting chat details fails""" session = create_chat_session() # Mock the repository to simulate chat details failure def mock_get_chat_detail(*args, **kwargs): return None with patch('app.repositories.chat.ChatRepository.get_chat_detail', side_effect=mock_get_chat_detail): response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert "Failed to get chat details after takeover" in response.json()["detail"] def test_takeover_chat_unexpected_error(client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session): """Test chat takeover with unexpected error""" session = create_chat_session() # Mock the repository to simulate unexpected error def mock_get_session(*args, **kwargs): raise Exception("Unexpected error occurred") with patch('app.repositories.session_to_agent.SessionToAgentRepository.get_session', side_effect=mock_get_session): response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert "Failed to take over chat" in response.json()["detail"] def test_takeover_chat_already_taken(client, db, user_with_manage_chats_permission, create_chat_session): """Test chat takeover when session is already taken""" session = create_chat_session() # First takeover should succeed response1 = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response1.status_code == status.HTTP_200_OK # Second takeover should fail response2 = client.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response2.status_code == status.HTTP_400_BAD_REQUEST assert "Failed to take over chat" in response2.json()["detail"] def test_takeover_chat_closed_session(client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session): """Test chat takeover when session is closed""" session = create_chat_session() # Close the session session_repo = SessionToAgentRepository(db) session_repo.update_session_status(session.session_id, SessionStatus.CLOSED) # Mock the repository to simulate takeover failure def mock_takeover_session(*args, **kwargs): return False with patch('app.repositories.session_to_agent.SessionToAgentRepository.takeover_session', side_effect=mock_takeover_session): response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/takeover" ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Failed to take over chat" in response.json()["detail"] @pytest.fixture def other_org_user(db, test_role_with_manage_chats) -> User: """A user (and their org) entirely separate from the caller's.""" other_org = Organization( id=uuid4(), name="Other Organization", domain="other.example.com", business_hours={}, settings={} ) db.add(other_org) db.commit() user = User( id=uuid4(), email="outsider@example.com", hashed_password="hashed_password", is_active=True, organization_id=other_org.id, full_name="Outsider", role_id=test_role_with_manage_chats.id ) db.add(user) db.commit() db.refresh(user) return user def _assigned_session(db, create_chat_session, user): """An open session already handled by a human — reassignable.""" session = create_chat_session() session.user_id = user.id db.commit() db.refresh(session) return session def test_reassign_chat_success(client_with_error_mock, db, user_with_manage_chats_permission, user_with_manage_assigned_chats, create_chat_session): """Reassigning to a colleague in the same org pushes them a notification""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) with patch('app.api.session_to_agent.notify_chat_assigned') as mock_notify: mock_notify.return_value = None response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(user_with_manage_assigned_chats.id)} ) assert response.status_code == status.HTTP_200_OK db.refresh(session) assert session.user_id == user_with_manage_assigned_chats.id assert mock_notify.called def test_reassign_refuses_a_same_org_chat_outside_the_callers_scope( client_with_error_mock, db, user_with_manage_chats_permission, user_with_manage_assigned_chats, create_chat_session ): """A scoped agent cannot steal another agent's chat by reassigning it.""" from unittest.mock import AsyncMock owner = user_with_manage_chats_permission caller = user_with_manage_assigned_chats session = _assigned_session(db, create_chat_session, owner) app.dependency_overrides[get_current_user] = lambda: caller try: with patch('app.api.session_to_agent.ChatRepository.check_session_access', new=AsyncMock(return_value=False)) as check_access, \ patch('app.api.session_to_agent.SessionToAgentRepository.reassign_session') as reassign: response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(caller.id)} ) finally: app.dependency_overrides[get_current_user] = lambda: owner assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json()["detail"] == "Chat session not found" check_access.assert_awaited_once() reassign.assert_not_called() db.refresh(session) assert session.user_id == owner.id def test_reassign_notifies_previous_assignee_not_new_one( client_with_error_mock, db, user_with_manage_chats_permission, user_with_manage_assigned_chats, create_chat_session ): """The 'reassigned_from_you' event must target the PREVIOUS owner's room. reassign_session mutates the loaded session in place, so reading session.user_id after it would wrongly point at the new assignee. """ from unittest.mock import AsyncMock previous = user_with_manage_chats_permission new = user_with_manage_assigned_chats session = _assigned_session(db, create_chat_session, previous) with patch('app.api.session_to_agent.notify_chat_assigned', new=AsyncMock()), \ patch('app.api.session_to_agent.sio.emit', new=AsyncMock()) as mock_emit: response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(new.id)} ) assert response.status_code == status.HTTP_200_OK from_you = [ c for c in mock_emit.call_args_list if c.args and isinstance(c.args[1], dict) and c.args[1].get('type') == 'reassigned_from_you' ] assert len(from_you) == 1, "expected exactly one reassigned_from_you emit" assert from_you[0].kwargs.get('room') == f"user_{previous.id}" assert from_you[0].kwargs.get('room') != f"user_{new.id}" def test_reassign_to_same_user_skips_from_you_event( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """Reassigning a chat to its current owner is a no-op — no contradictory ping.""" from unittest.mock import AsyncMock owner = user_with_manage_chats_permission session = _assigned_session(db, create_chat_session, owner) with patch('app.api.session_to_agent.notify_chat_assigned', new=AsyncMock()), \ patch('app.api.session_to_agent.sio.emit', new=AsyncMock()) as mock_emit: response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(owner.id)} ) assert response.status_code == status.HTTP_200_OK from_you = [ c for c in mock_emit.call_args_list if c.args and isinstance(c.args[1], dict) and c.args[1].get('type') == 'reassigned_from_you' ] assert from_you == [] def test_reassign_chat_other_org_session(client_with_error_mock, db, other_org_user, user_with_manage_assigned_chats, user_with_manage_chats_permission, create_chat_session): """A session belonging to another org is invisible, not reassignable""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) session.organization_id = other_org_user.organization_id db.commit() response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(user_with_manage_assigned_chats.id)} ) assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json()["detail"] == "Chat session not found" db.refresh(session) assert session.user_id == user_with_manage_chats_permission.id def test_reassign_chat_target_in_another_org(client_with_error_mock, db, other_org_user, user_with_manage_chats_permission, create_chat_session): """A chat can't be handed to someone outside the caller's org""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(other_org_user.id)} ) assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json()["detail"] == "User not found" db.refresh(session) assert session.user_id == user_with_manage_chats_permission.id def test_reassign_chat_unknown_target(client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session): """An unknown user id is a 404, and a malformed one isn't a 500""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) for target in (str(uuid4()), "not-a-uuid"): response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": target} ) assert response.status_code == status.HTTP_404_NOT_FOUND, target assert response.json()["detail"] == "User not found" def test_reassign_chat_no_permission(client_with_error_mock, db, regular_user, user_with_manage_chats_permission, user_with_manage_assigned_chats, create_chat_session): """Without a manage grant, reassignment is forbidden""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) async def override_get_current_user(): return regular_user app.dependency_overrides[get_current_user] = override_get_current_user try: response = client_with_error_mock.post( f"/api/v1/session-to-agent/{session.session_id}/reassign", params={"to_user_id": str(user_with_manage_assigned_chats.id)} ) finally: app.dependency_overrides[get_current_user] = lambda: user_with_manage_chats_permission assert response.status_code == status.HTTP_403_FORBIDDEN def _route_to_human(client, session): return client.post(f"/api/v1/session-to-agent/{session.session_id}/route-to-human") def test_route_to_human_queues_the_chat_without_claiming_it( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """The AI stops answering, but the chat stays claimable by anyone.""" from unittest.mock import AsyncMock session = create_chat_session() with patch('app.services.human_routing.notify_chat_event', new=AsyncMock()) as mock_notify: response = _route_to_human(client_with_error_mock, session) assert response.status_code == status.HTTP_200_OK db.refresh(session) assert session.status == SessionStatus.TRANSFERRED # Not claimed: every backend guard reads "a human has this" off user_id. assert session.user_id is None assert mock_notify.called def test_route_to_human_is_idempotent( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """A second click must not re-notify the team.""" from unittest.mock import AsyncMock session = create_chat_session() with patch('app.services.human_routing.notify_chat_event', new=AsyncMock()): assert _route_to_human(client_with_error_mock, session).status_code == status.HTTP_200_OK with patch('app.services.human_routing.notify_chat_event', new=AsyncMock()) as mock_notify: response = _route_to_human(client_with_error_mock, session) assert response.status_code == status.HTTP_400_BAD_REQUEST assert not mock_notify.called def test_route_to_human_refuses_a_chat_a_human_holds( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """Someone is already on it — there is no AI to stop.""" session = _assigned_session(db, create_chat_session, user_with_manage_chats_permission) response = _route_to_human(client_with_error_mock, session) assert response.status_code == status.HTTP_400_BAD_REQUEST db.refresh(session) assert session.user_id == user_with_manage_chats_permission.id def test_route_to_human_refuses_a_closed_chat( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): session = create_chat_session() session.status = SessionStatus.CLOSED db.commit() response = _route_to_human(client_with_error_mock, session) assert response.status_code == status.HTTP_400_BAD_REQUEST db.refresh(session) assert session.status == SessionStatus.CLOSED def test_route_to_human_no_permission( client_with_error_mock, db, regular_user, user_with_manage_chats_permission, create_chat_session ): session = create_chat_session() async def override_get_current_user(): return regular_user app.dependency_overrides[get_current_user] = override_get_current_user try: response = _route_to_human(client_with_error_mock, session) finally: app.dependency_overrides[get_current_user] = lambda: user_with_manage_chats_permission assert response.status_code == status.HTTP_403_FORBIDDEN db.refresh(session) assert session.status == SessionStatus.OPEN def test_route_to_human_other_org_session( client_with_error_mock, db, other_org_user, user_with_manage_chats_permission, create_chat_session ): """A guessable session id must not let an outsider touch the chat.""" session = create_chat_session() async def override_get_current_user(): return other_org_user app.dependency_overrides[get_current_user] = override_get_current_user try: response = _route_to_human(client_with_error_mock, session) finally: app.dependency_overrides[get_current_user] = lambda: user_with_manage_chats_permission assert response.status_code == status.HTTP_404_NOT_FOUND db.refresh(session) assert session.status == SessionStatus.OPEN def test_takeover_notifies_a_channel_customer_but_not_the_widget( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """The widget shows handover in its own UI; only channels need telling. Both cases share one helper now, so this pins the skip in place. """ from unittest.mock import AsyncMock from app.services.message_delivery import DeliveryResult web_session = create_chat_session() channel_session = create_chat_session() channel_session.channel = 'telegram' db.commit() with patch('app.services.human_routing.deliver_to_customer', new=AsyncMock(return_value=DeliveryResult(ok=True))) as mock_deliver: client_with_error_mock.post( f"/api/v1/session-to-agent/{web_session.session_id}/takeover") assert not mock_deliver.called client_with_error_mock.post( f"/api/v1/session-to-agent/{channel_session.session_id}/takeover") assert mock_deliver.called def test_route_to_human_tells_a_channel_customer_someone_is_coming( client_with_error_mock, db, user_with_manage_chats_permission, create_chat_session ): """Queued is not the same as connected — the holding line says so.""" from unittest.mock import AsyncMock from app.services.human_routing import QUEUED_FOR_HUMAN_NOTICE from app.services.message_delivery import DeliveryResult session = create_chat_session() session.channel = 'telegram' db.commit() with patch('app.services.human_routing.notify_chat_event', new=AsyncMock()), \ patch('app.services.human_routing.deliver_to_customer', new=AsyncMock(return_value=DeliveryResult(ok=True))) as mock_deliver: response = _route_to_human(client_with_error_mock, session) assert response.status_code == status.HTTP_200_OK assert mock_deliver.called assert mock_deliver.call_args.args[2]['message'] == QUEUED_FOR_HUMAN_NOTICE