"""Tests for app.escalation module.""" from __future__ import annotations from unittest.mock import AsyncMock, patch import httpx import pytest from app.escalation import ( EscalationPayload, EscalationResult, NoOpEscalator, WebhookEscalator, ) def _make_payload(**kwargs) -> EscalationPayload: defaults = { "thread_id": "t1", "reason": "Agent cannot resolve", "conversation_summary": "User asked about refund policy", } defaults.update(kwargs) return EscalationPayload(**defaults) @pytest.mark.unit class TestEscalationPayload: def test_frozen(self) -> None: payload = _make_payload() with pytest.raises(Exception): payload.thread_id = "t2" # type: ignore[misc] def test_default_metadata(self) -> None: payload = _make_payload() assert payload.metadata == {} def test_model_dump(self) -> None: payload = _make_payload(metadata={"key": "val"}) data = payload.model_dump() assert data["thread_id"] == "t1" assert data["metadata"] == {"key": "val"} @pytest.mark.unit class TestEscalationResult: def test_frozen(self) -> None: result = EscalationResult(success=True, status_code=200, attempts=1, error=None) assert result.success assert result.status_code == 200 @pytest.mark.unit class TestWebhookEscalator: @pytest.mark.asyncio async def test_empty_url_returns_failure(self) -> None: escalator = WebhookEscalator(url="", max_retries=3) result = await escalator.escalate(_make_payload()) assert not result.success assert result.attempts == 0 assert "not configured" in result.error @pytest.mark.asyncio async def test_successful_post(self) -> None: mock_response = AsyncMock() mock_response.status_code = 200 mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with patch("app.escalation.httpx.AsyncClient", return_value=mock_client): escalator = WebhookEscalator(url="https://example.com/hook") result = await escalator.escalate(_make_payload()) assert result.success assert result.status_code == 200 assert result.attempts == 1 @pytest.mark.asyncio async def test_retry_on_server_error(self) -> None: fail_response = AsyncMock() fail_response.status_code = 500 success_response = AsyncMock() success_response.status_code = 200 mock_client = AsyncMock() mock_client.post = AsyncMock(side_effect=[fail_response, fail_response, success_response]) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with ( patch("app.escalation.httpx.AsyncClient", return_value=mock_client), patch("app.escalation.asyncio.sleep", new_callable=AsyncMock), ): escalator = WebhookEscalator(url="https://example.com/hook", max_retries=3) result = await escalator.escalate(_make_payload()) assert result.success assert result.attempts == 3 @pytest.mark.asyncio async def test_all_retries_exhausted(self) -> None: fail_response = AsyncMock() fail_response.status_code = 500 mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=fail_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with ( patch("app.escalation.httpx.AsyncClient", return_value=mock_client), patch("app.escalation.asyncio.sleep", new_callable=AsyncMock), ): escalator = WebhookEscalator(url="https://example.com/hook", max_retries=3) result = await escalator.escalate(_make_payload()) assert not result.success assert result.attempts == 3 assert "500" in result.error @pytest.mark.asyncio async def test_timeout_error(self) -> None: mock_client = AsyncMock() mock_client.post = AsyncMock(side_effect=httpx.TimeoutException("timeout")) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with ( patch("app.escalation.httpx.AsyncClient", return_value=mock_client), patch("app.escalation.asyncio.sleep", new_callable=AsyncMock), ): escalator = WebhookEscalator(url="https://example.com/hook", max_retries=2) result = await escalator.escalate(_make_payload()) assert not result.success assert "timed out" in result.error @pytest.mark.asyncio async def test_request_error(self) -> None: mock_client = AsyncMock() mock_client.post = AsyncMock( side_effect=httpx.RequestError("connection refused") ) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with ( patch("app.escalation.httpx.AsyncClient", return_value=mock_client), patch("app.escalation.asyncio.sleep", new_callable=AsyncMock), ): escalator = WebhookEscalator(url="https://example.com/hook", max_retries=1) result = await escalator.escalate(_make_payload()) assert not result.success @pytest.mark.unit class TestNoOpEscalator: @pytest.mark.asyncio async def test_returns_disabled(self) -> None: escalator = NoOpEscalator() result = await escalator.escalate(_make_payload()) assert not result.success assert result.attempts == 0 assert "disabled" in result.error.lower()