Skip to content

Commit 49597ef

Browse files
committed
fix(client): preserve canonical format options
1 parent b6c05c5 commit 49597ef

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

src/adcp/client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,24 @@ def _canonicalize_get_products_result(
15061506
) -> TaskResult[GetProductsResponse]:
15071507
"""Parse the wire shape, project products, and enforce the primary boundary."""
15081508

1509+
# Canonical responses must be parsed before the legacy compatibility
1510+
# model. The generated legacy ProductFormatDeclaration intentionally
1511+
# lacks canonical ``format_kind`` and ``params`` fields, so parsing a
1512+
# canonical-only response through it first irreversibly discards the
1513+
# declaration before ``project_legacy_product`` can inspect it.
1514+
canonical_result: TaskResult[GetProductsResponse] = self.adapter._parse_response(
1515+
raw_result, GetProductsResponse
1516+
)
1517+
if not raw_result.success or raw_result.data is None:
1518+
return canonical_result
1519+
if canonical_result.success and canonical_result.data is not None:
1520+
direct_products = list(canonical_result.data.products or [])
1521+
self._remember_canonical_product_routes(direct_products)
1522+
metadata = dict(canonical_result.metadata or {})
1523+
metadata["projection"] = {"diagnostics": []}
1524+
canonical_result.metadata = metadata
1525+
return canonical_result
1526+
15091527
legacy_result: TaskResult[Any] = self.adapter._parse_response(
15101528
raw_result, LegacyGetProductsResponse
15111529
)

tests/test_client.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
11
"""Tests for ADCPClient."""
22

3+
from typing import Any
4+
35
import pytest
46

57
from adcp import ADCPClient, ADCPMultiAgentClient
68
from adcp.types import AgentConfig, Protocol
79
from tests.conftest import validate_union
810

911

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+
1046
def test_agent_config_creation():
1147
"""Test creating agent configuration."""
1248
config = AgentConfig(
@@ -165,7 +201,7 @@ async def test_get_products():
165201
"""Test get_products method with mock adapter."""
166202
from unittest.mock import patch
167203

168-
from adcp.types import GetProductsRequest, GetProductsResponse, LegacyGetProductsResponse
204+
from adcp.types import GetProductsRequest, GetProductsResponse
169205
from adcp.types.core import TaskResult, TaskStatus
170206

171207
config = AgentConfig(
@@ -203,13 +239,92 @@ async def test_get_products():
203239
# Verify adapter method was called
204240
mock_get.assert_called_once_with({"brief": "test campaign", "buying_mode": "brief"})
205241
# 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)
207243
# Verify final result
208244
assert result.success is True
209245
assert result.status == TaskStatus.COMPLETED
210246
assert isinstance(result.data, GetProductsResponse)
211247

212248

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+
213328
@pytest.mark.asyncio
214329
async def test_get_products_wholesale_versions_sent_and_parsed():
215330
"""Wholesale product enumeration sends and parses beta 3 version tokens."""

0 commit comments

Comments
 (0)