|
1 | 1 | """Tests for ADCPClient.""" |
2 | 2 |
|
| 3 | +from typing import Any |
| 4 | + |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | from adcp import ADCPClient, ADCPMultiAgentClient |
6 | 8 | from adcp.types import AgentConfig, Protocol |
7 | 9 | from tests.conftest import validate_union |
8 | 10 |
|
9 | 11 |
|
| 12 | +def _get_products_product( |
| 13 | + *, |
| 14 | + format_options: list[dict[str, Any]] | None = None, |
| 15 | + format_ids: list[dict[str, str]] | None = None, |
| 16 | +) -> dict[str, Any]: |
| 17 | + product: dict[str, Any] = { |
| 18 | + "product_id": "p1", |
| 19 | + "name": "Product 1", |
| 20 | + "description": "A test product", |
| 21 | + "publisher_properties": [{"selection_type": "all", "publisher_domain": "pub.example.com"}], |
| 22 | + "delivery_type": "non_guaranteed", |
| 23 | + "pricing_options": [ |
| 24 | + { |
| 25 | + "pricing_model": "cpm", |
| 26 | + "pricing_option_id": "po1", |
| 27 | + "currency": "USD", |
| 28 | + } |
| 29 | + ], |
| 30 | + "reporting_capabilities": { |
| 31 | + "available_reporting_frequencies": ["daily"], |
| 32 | + "expected_delay_minutes": 0, |
| 33 | + "timezone": "UTC", |
| 34 | + "supports_webhooks": False, |
| 35 | + "available_metrics": ["impressions"], |
| 36 | + "date_range_support": "date_range", |
| 37 | + }, |
| 38 | + } |
| 39 | + if format_options is not None: |
| 40 | + product["format_options"] = format_options |
| 41 | + if format_ids is not None: |
| 42 | + product["format_ids"] = format_ids |
| 43 | + return product |
| 44 | + |
| 45 | + |
10 | 46 | def test_agent_config_creation(): |
11 | 47 | """Test creating agent configuration.""" |
12 | 48 | config = AgentConfig( |
@@ -165,7 +201,7 @@ async def test_get_products(): |
165 | 201 | """Test get_products method with mock adapter.""" |
166 | 202 | from unittest.mock import patch |
167 | 203 |
|
168 | | - from adcp.types import GetProductsRequest, GetProductsResponse, LegacyGetProductsResponse |
| 204 | + from adcp.types import GetProductsRequest, GetProductsResponse |
169 | 205 | from adcp.types.core import TaskResult, TaskStatus |
170 | 206 |
|
171 | 207 | config = AgentConfig( |
@@ -203,13 +239,92 @@ async def test_get_products(): |
203 | 239 | # Verify adapter method was called |
204 | 240 | mock_get.assert_called_once_with({"brief": "test campaign", "buying_mode": "brief"}) |
205 | 241 | # Verify parsing was called with correct type |
206 | | - mock_parse.assert_called_once_with(mock_raw_result, LegacyGetProductsResponse) |
| 242 | + mock_parse.assert_called_once_with(mock_raw_result, GetProductsResponse) |
207 | 243 | # Verify final result |
208 | 244 | assert result.success is True |
209 | 245 | assert result.status == TaskStatus.COMPLETED |
210 | 246 | assert isinstance(result.data, GetProductsResponse) |
211 | 247 |
|
212 | 248 |
|
| 249 | +def test_get_products_preserves_canonical_format_options(): |
| 250 | + """Canonical declarations must not pass through the lossy legacy model.""" |
| 251 | + from adcp.types import GetProductsResponse |
| 252 | + from adcp.types.core import TaskResult, TaskStatus |
| 253 | + |
| 254 | + config = AgentConfig( |
| 255 | + id="test_agent", |
| 256 | + agent_uri="https://test.example.com", |
| 257 | + protocol=Protocol.A2A, |
| 258 | + ) |
| 259 | + client = ADCPClient(config) |
| 260 | + raw_result = TaskResult( |
| 261 | + status=TaskStatus.COMPLETED, |
| 262 | + success=True, |
| 263 | + data={ |
| 264 | + "products": [ |
| 265 | + _get_products_product( |
| 266 | + format_options=[ |
| 267 | + { |
| 268 | + "format_option_id": "p1-display", |
| 269 | + "format_kind": "image", |
| 270 | + "params": {"width": 300, "height": 250}, |
| 271 | + } |
| 272 | + ] |
| 273 | + ) |
| 274 | + ] |
| 275 | + }, |
| 276 | + ) |
| 277 | + |
| 278 | + result = client._canonicalize_get_products_result(raw_result) |
| 279 | + |
| 280 | + assert result.success is True |
| 281 | + assert isinstance(result.data, GetProductsResponse) |
| 282 | + assert result.data.products is not None |
| 283 | + assert len(result.data.products) == 1 |
| 284 | + declaration = result.data.products[0].format_options[0] |
| 285 | + assert declaration.format_kind.value == "image" |
| 286 | + assert declaration.params == {"width": 300, "height": 250} |
| 287 | + assert result.metadata == {"projection": {"diagnostics": []}} |
| 288 | + |
| 289 | + |
| 290 | +def test_get_products_still_projects_legacy_format_ids(): |
| 291 | + """Canonical-first parsing must retain the legacy compatibility fallback.""" |
| 292 | + from adcp.types import GetProductsResponse |
| 293 | + from adcp.types.core import TaskResult, TaskStatus |
| 294 | + |
| 295 | + client = ADCPClient( |
| 296 | + AgentConfig( |
| 297 | + id="test_agent", |
| 298 | + agent_uri="https://test.example.com", |
| 299 | + protocol=Protocol.A2A, |
| 300 | + ) |
| 301 | + ) |
| 302 | + raw_result = TaskResult( |
| 303 | + status=TaskStatus.COMPLETED, |
| 304 | + success=True, |
| 305 | + data={ |
| 306 | + "products": [ |
| 307 | + _get_products_product( |
| 308 | + format_ids=[ |
| 309 | + { |
| 310 | + "agent_url": "https://seller.example", |
| 311 | + "id": "display_300x250_image", |
| 312 | + } |
| 313 | + ] |
| 314 | + ) |
| 315 | + ] |
| 316 | + }, |
| 317 | + ) |
| 318 | + |
| 319 | + result = client._canonicalize_get_products_result(raw_result) |
| 320 | + |
| 321 | + assert result.success is True |
| 322 | + assert isinstance(result.data, GetProductsResponse) |
| 323 | + assert result.data.products is not None |
| 324 | + assert len(result.data.products) == 1 |
| 325 | + assert result.data.products[0].format_options[0].format_kind.value == "image" |
| 326 | + |
| 327 | + |
213 | 328 | @pytest.mark.asyncio |
214 | 329 | async def test_get_products_wholesale_versions_sent_and_parsed(): |
215 | 330 | """Wholesale product enumeration sends and parses beta 3 version tokens.""" |
|
0 commit comments