|
1 | 1 | """Tests for escalation_tool.py metadata.""" |
2 | 2 |
|
| 3 | +from unittest.mock import AsyncMock, MagicMock, patch |
| 4 | + |
3 | 5 | import pytest |
4 | 6 | from uipath.agent.models.agent import ( |
5 | 7 | AgentEscalationChannel, |
6 | 8 | AgentEscalationChannelProperties, |
7 | 9 | AgentEscalationRecipientType, |
8 | 10 | AgentEscalationResourceConfig, |
| 11 | + AssetRecipient, |
9 | 12 | StandardRecipient, |
10 | 13 | ) |
11 | 14 |
|
12 | | -from uipath_langchain.agent.tools.escalation_tool import create_escalation_tool |
| 15 | +from uipath_langchain.agent.tools.escalation_tool import ( |
| 16 | + create_escalation_tool, |
| 17 | + resolve_asset, |
| 18 | + resolve_recipient_value, |
| 19 | +) |
| 20 | + |
| 21 | + |
| 22 | +class TestResolveAsset: |
| 23 | + """Test the resolve_asset function.""" |
| 24 | + |
| 25 | + @pytest.mark.asyncio |
| 26 | + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") |
| 27 | + async def test_resolve_asset_success(self, mock_uipath_class): |
| 28 | + """Test successful asset retrieval.""" |
| 29 | + # Setup mock |
| 30 | + mock_client = MagicMock() |
| 31 | + mock_uipath_class.return_value = mock_client |
| 32 | + mock_result = MagicMock() |
| 33 | + mock_result.value = "test@example.com" |
| 34 | + mock_client.assets.retrieve_async = AsyncMock(return_value=mock_result) |
| 35 | + |
| 36 | + # Execute |
| 37 | + result = await resolve_asset("email_asset", "/Test/Folder") |
| 38 | + |
| 39 | + # Assert |
| 40 | + assert result == "test@example.com" |
| 41 | + mock_client.assets.retrieve_async.assert_called_once_with( |
| 42 | + name="email_asset", folder_path="/Test/Folder" |
| 43 | + ) |
| 44 | + |
| 45 | + @pytest.mark.asyncio |
| 46 | + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") |
| 47 | + async def test_resolve_asset_no_value(self, mock_uipath_class): |
| 48 | + """Test asset with no value raises ValueError.""" |
| 49 | + # Setup mock |
| 50 | + mock_client = MagicMock() |
| 51 | + mock_uipath_class.return_value = mock_client |
| 52 | + mock_result = MagicMock() |
| 53 | + mock_result.value = None |
| 54 | + mock_client.assets.retrieve_async = AsyncMock(return_value=mock_result) |
| 55 | + |
| 56 | + # Execute and assert |
| 57 | + with pytest.raises(ValueError) as exc_info: |
| 58 | + await resolve_asset("empty_asset", "/Test/Folder") |
| 59 | + |
| 60 | + assert "Asset 'empty_asset' has no value configured" in str(exc_info.value) |
| 61 | + |
| 62 | + @pytest.mark.asyncio |
| 63 | + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") |
| 64 | + async def test_resolve_asset_not_found(self, mock_uipath_class): |
| 65 | + """Test asset not found raises ValueError.""" |
| 66 | + # Setup mock |
| 67 | + mock_client = MagicMock() |
| 68 | + mock_uipath_class.return_value = mock_client |
| 69 | + mock_client.assets.retrieve_async = AsyncMock(return_value=None) |
| 70 | + |
| 71 | + # Execute and assert |
| 72 | + with pytest.raises(ValueError) as exc_info: |
| 73 | + await resolve_asset("missing_asset", "/Test/Folder") |
| 74 | + |
| 75 | + assert "Asset 'missing_asset' has no value configured" in str(exc_info.value) |
| 76 | + |
| 77 | + @pytest.mark.asyncio |
| 78 | + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") |
| 79 | + async def test_resolve_asset_retrieval_exception(self, mock_uipath_class): |
| 80 | + """Test exception during asset retrieval raises ValueError with context.""" |
| 81 | + # Setup mock |
| 82 | + mock_client = MagicMock() |
| 83 | + mock_uipath_class.return_value = mock_client |
| 84 | + mock_client.assets.retrieve_async = AsyncMock( |
| 85 | + side_effect=Exception("Connection error") |
| 86 | + ) |
| 87 | + |
| 88 | + # Execute and assert |
| 89 | + with pytest.raises(ValueError) as exc_info: |
| 90 | + await resolve_asset("problem_asset", "/Test/Folder") |
| 91 | + |
| 92 | + assert ( |
| 93 | + "Failed to resolve asset 'problem_asset' in folder '/Test/Folder'" |
| 94 | + in str(exc_info.value) |
| 95 | + ) |
| 96 | + assert "Connection error" in str(exc_info.value) |
| 97 | + |
| 98 | + |
| 99 | +class TestResolveRecipientValue: |
| 100 | + """Test the resolve_recipient_value function.""" |
| 101 | + |
| 102 | + @pytest.mark.asyncio |
| 103 | + @patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset") |
| 104 | + async def test_resolve_recipient_asset_user_email(self, mock_resolve_asset): |
| 105 | + """Test ASSET_USER_EMAIL type calls resolve_asset.""" |
| 106 | + mock_resolve_asset.return_value = "resolved@example.com" |
| 107 | + |
| 108 | + recipient = AssetRecipient( |
| 109 | + type=AgentEscalationRecipientType.ASSET_USER_EMAIL, |
| 110 | + asset_name="email_asset", |
| 111 | + folder_path="/Test/Folder", |
| 112 | + ) |
| 113 | + |
| 114 | + result = await resolve_recipient_value(recipient) |
| 115 | + |
| 116 | + assert result == "resolved@example.com" |
| 117 | + mock_resolve_asset.assert_called_once_with("email_asset", "/Test/Folder") |
| 118 | + |
| 119 | + @pytest.mark.asyncio |
| 120 | + @patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset") |
| 121 | + async def test_resolve_recipient_asset_group_name(self, mock_resolve_asset): |
| 122 | + """Test ASSET_GROUP_NAME type calls resolve_asset.""" |
| 123 | + mock_resolve_asset.return_value = "ResolvedGroup" |
| 124 | + |
| 125 | + recipient = AssetRecipient( |
| 126 | + type=AgentEscalationRecipientType.ASSET_GROUP_NAME, |
| 127 | + asset_name="group_asset", |
| 128 | + folder_path="/Test/Folder", |
| 129 | + ) |
| 130 | + |
| 131 | + result = await resolve_recipient_value(recipient) |
| 132 | + |
| 133 | + assert result == "ResolvedGroup" |
| 134 | + mock_resolve_asset.assert_called_once_with("group_asset", "/Test/Folder") |
| 135 | + |
| 136 | + @pytest.mark.asyncio |
| 137 | + async def test_resolve_recipient_user_email(self): |
| 138 | + """Test USER_EMAIL type returns value directly.""" |
| 139 | + recipient = StandardRecipient( |
| 140 | + type=AgentEscalationRecipientType.USER_EMAIL, |
| 141 | + value="direct@example.com", |
| 142 | + ) |
| 143 | + |
| 144 | + result = await resolve_recipient_value(recipient) |
| 145 | + |
| 146 | + assert result == "direct@example.com" |
| 147 | + |
| 148 | + @pytest.mark.asyncio |
| 149 | + @patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset") |
| 150 | + async def test_resolve_recipient_propagates_error_when_asset_resolution_fails( |
| 151 | + self, mock_resolve_asset |
| 152 | + ): |
| 153 | + """Test AssetRecipient when asset resolution fails.""" |
| 154 | + mock_resolve_asset.side_effect = ValueError("Asset not found") |
| 155 | + |
| 156 | + recipient = AssetRecipient( |
| 157 | + type=AgentEscalationRecipientType.ASSET_USER_EMAIL, |
| 158 | + asset_name="nonexistent", |
| 159 | + folder_path="Shared", |
| 160 | + ) |
| 161 | + |
| 162 | + with pytest.raises(ValueError) as exc_info: |
| 163 | + await resolve_recipient_value(recipient) |
| 164 | + |
| 165 | + assert "Asset not found" in str(exc_info.value) |
| 166 | + |
| 167 | + @pytest.mark.asyncio |
| 168 | + async def test_resolve_recipient_no_value(self): |
| 169 | + """Test recipient without value attribute returns None.""" |
| 170 | + # Create a minimal recipient object without value |
| 171 | + recipient = MagicMock() |
| 172 | + recipient.type = AgentEscalationRecipientType.USER_EMAIL |
| 173 | + del recipient.value # Simulate no value attribute |
| 174 | + |
| 175 | + result = await resolve_recipient_value(recipient) |
| 176 | + |
| 177 | + assert result is None |
13 | 178 |
|
14 | 179 |
|
15 | 180 | class TestEscalationToolMetadata: |
@@ -66,41 +231,47 @@ def escalation_resource_no_recipient(self): |
66 | 231 | ], |
67 | 232 | ) |
68 | 233 |
|
69 | | - def test_escalation_tool_has_metadata(self, escalation_resource): |
| 234 | + @pytest.mark.asyncio |
| 235 | + async def test_escalation_tool_has_metadata(self, escalation_resource): |
70 | 236 | """Test that escalation tool has metadata dict.""" |
71 | | - tool = create_escalation_tool(escalation_resource) |
| 237 | + tool = await create_escalation_tool(escalation_resource) |
72 | 238 |
|
73 | 239 | assert tool.metadata is not None |
74 | 240 | assert isinstance(tool.metadata, dict) |
75 | 241 |
|
76 | | - def test_escalation_tool_metadata_has_tool_type(self, escalation_resource): |
| 242 | + @pytest.mark.asyncio |
| 243 | + async def test_escalation_tool_metadata_has_tool_type(self, escalation_resource): |
77 | 244 | """Test that metadata contains tool_type for span detection.""" |
78 | | - tool = create_escalation_tool(escalation_resource) |
| 245 | + tool = await create_escalation_tool(escalation_resource) |
79 | 246 | assert tool.metadata is not None |
80 | 247 | assert tool.metadata["tool_type"] == "escalation" |
81 | 248 |
|
82 | | - def test_escalation_tool_metadata_has_display_name(self, escalation_resource): |
| 249 | + @pytest.mark.asyncio |
| 250 | + async def test_escalation_tool_metadata_has_display_name(self, escalation_resource): |
83 | 251 | """Test that metadata contains display_name from app_name.""" |
84 | | - tool = create_escalation_tool(escalation_resource) |
| 252 | + tool = await create_escalation_tool(escalation_resource) |
85 | 253 | assert tool.metadata is not None |
86 | 254 | assert tool.metadata["display_name"] == "ApprovalApp" |
87 | 255 |
|
88 | | - def test_escalation_tool_metadata_has_channel_type(self, escalation_resource): |
| 256 | + @pytest.mark.asyncio |
| 257 | + async def test_escalation_tool_metadata_has_channel_type(self, escalation_resource): |
89 | 258 | """Test that metadata contains channel_type for span attributes.""" |
90 | | - tool = create_escalation_tool(escalation_resource) |
| 259 | + tool = await create_escalation_tool(escalation_resource) |
91 | 260 | assert tool.metadata is not None |
92 | 261 | assert tool.metadata["channel_type"] == "actionCenter" |
93 | 262 |
|
94 | | - def test_escalation_tool_metadata_has_assignee(self, escalation_resource): |
| 263 | + @pytest.mark.asyncio |
| 264 | + async def test_escalation_tool_metadata_has_assignee(self, escalation_resource): |
95 | 265 | """Test that metadata contains assignee when recipient is USER_EMAIL.""" |
96 | | - tool = create_escalation_tool(escalation_resource) |
| 266 | + tool = await create_escalation_tool(escalation_resource) |
97 | 267 | assert tool.metadata is not None |
98 | 268 | assert tool.metadata["assignee"] == "user@example.com" |
99 | 269 |
|
100 | | - def test_escalation_tool_metadata_assignee_none_when_no_recipients( |
| 270 | + @pytest.mark.asyncio |
| 271 | + async def test_escalation_tool_metadata_assignee_none_when_no_recipients( |
101 | 272 | self, escalation_resource_no_recipient |
102 | 273 | ): |
103 | 274 | """Test that assignee is None when no recipients configured.""" |
104 | | - tool = create_escalation_tool(escalation_resource_no_recipient) |
| 275 | + tool = await create_escalation_tool(escalation_resource_no_recipient) |
105 | 276 | assert tool.metadata is not None |
106 | 277 | assert tool.metadata["assignee"] is None |
0 commit comments