Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/adcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,24 @@ def _canonicalize_get_products_result(
) -> TaskResult[GetProductsResponse]:
"""Parse the wire shape, project products, and enforce the primary boundary."""

# Canonical responses must be parsed before the legacy compatibility
# model. The generated legacy ProductFormatDeclaration intentionally
# lacks canonical ``format_kind`` and ``params`` fields, so parsing a
# canonical-only response through it first irreversibly discards the
# declaration before ``project_legacy_product`` can inspect it.
canonical_result: TaskResult[GetProductsResponse] = self.adapter._parse_response(
raw_result, GetProductsResponse
)
if not raw_result.success or raw_result.data is None:
return canonical_result
if canonical_result.success and canonical_result.data is not None:
direct_products = list(canonical_result.data.products or [])
self._remember_canonical_product_routes(direct_products)
metadata = dict(canonical_result.metadata or {})
metadata["projection"] = {"diagnostics": []}
canonical_result.metadata = metadata
return canonical_result

legacy_result: TaskResult[Any] = self.adapter._parse_response(
raw_result, LegacyGetProductsResponse
)
Expand Down
119 changes: 117 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
"""Tests for ADCPClient."""

from typing import Any

import pytest

from adcp import ADCPClient, ADCPMultiAgentClient
from adcp.types import AgentConfig, Protocol
from tests.conftest import validate_union


def _get_products_product(
*,
format_options: list[dict[str, Any]] | None = None,
format_ids: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
product: dict[str, Any] = {
"product_id": "p1",
"name": "Product 1",
"description": "A test product",
"publisher_properties": [{"selection_type": "all", "publisher_domain": "pub.example.com"}],
"delivery_type": "non_guaranteed",
"pricing_options": [
{
"pricing_model": "cpm",
"pricing_option_id": "po1",
"currency": "USD",
}
],
"reporting_capabilities": {
"available_reporting_frequencies": ["daily"],
"expected_delay_minutes": 0,
"timezone": "UTC",
"supports_webhooks": False,
"available_metrics": ["impressions"],
"date_range_support": "date_range",
},
}
if format_options is not None:
product["format_options"] = format_options
if format_ids is not None:
product["format_ids"] = format_ids
return product


def test_agent_config_creation():
"""Test creating agent configuration."""
config = AgentConfig(
Expand Down Expand Up @@ -165,7 +201,7 @@ async def test_get_products():
"""Test get_products method with mock adapter."""
from unittest.mock import patch

from adcp.types import GetProductsRequest, GetProductsResponse, LegacyGetProductsResponse
from adcp.types import GetProductsRequest, GetProductsResponse
from adcp.types.core import TaskResult, TaskStatus

config = AgentConfig(
Expand Down Expand Up @@ -203,13 +239,92 @@ async def test_get_products():
# Verify adapter method was called
mock_get.assert_called_once_with({"brief": "test campaign", "buying_mode": "brief"})
# Verify parsing was called with correct type
mock_parse.assert_called_once_with(mock_raw_result, LegacyGetProductsResponse)
mock_parse.assert_called_once_with(mock_raw_result, GetProductsResponse)
# Verify final result
assert result.success is True
assert result.status == TaskStatus.COMPLETED
assert isinstance(result.data, GetProductsResponse)


def test_get_products_preserves_canonical_format_options():
"""Canonical declarations must not pass through the lossy legacy model."""
from adcp.types import GetProductsResponse
from adcp.types.core import TaskResult, TaskStatus

config = AgentConfig(
id="test_agent",
agent_uri="https://test.example.com",
protocol=Protocol.A2A,
)
client = ADCPClient(config)
raw_result = TaskResult(
status=TaskStatus.COMPLETED,
success=True,
data={
"products": [
_get_products_product(
format_options=[
{
"format_option_id": "p1-display",
"format_kind": "image",
"params": {"width": 300, "height": 250},
}
]
)
]
},
)

result = client._canonicalize_get_products_result(raw_result)

assert result.success is True
assert isinstance(result.data, GetProductsResponse)
assert result.data.products is not None
assert len(result.data.products) == 1
declaration = result.data.products[0].format_options[0]
assert declaration.format_kind.value == "image"
assert declaration.params == {"width": 300, "height": 250}
assert result.metadata == {"projection": {"diagnostics": []}}


def test_get_products_still_projects_legacy_format_ids():
"""Canonical-first parsing must retain the legacy compatibility fallback."""
from adcp.types import GetProductsResponse
from adcp.types.core import TaskResult, TaskStatus

client = ADCPClient(
AgentConfig(
id="test_agent",
agent_uri="https://test.example.com",
protocol=Protocol.A2A,
)
)
raw_result = TaskResult(
status=TaskStatus.COMPLETED,
success=True,
data={
"products": [
_get_products_product(
format_ids=[
{
"agent_url": "https://seller.example",
"id": "display_300x250_image",
}
]
)
]
},
)

result = client._canonicalize_get_products_result(raw_result)

assert result.success is True
assert isinstance(result.data, GetProductsResponse)
assert result.data.products is not None
assert len(result.data.products) == 1
assert result.data.products[0].format_options[0].format_kind.value == "image"


@pytest.mark.asyncio
async def test_get_products_wholesale_versions_sent_and_parsed():
"""Wholesale product enumeration sends and parses beta 3 version tokens."""
Expand Down
Loading