Skip to content
Merged
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
84 changes: 83 additions & 1 deletion examples/seller_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import Any

from adcp.server import (
INSECURE_ALLOW_ALL,
ADCPHandler,
adcp_error,
cancel_media_buy_response,
Expand All @@ -39,7 +40,6 @@
sync_governance_response,
update_media_buy_response,
)
from adcp.server import INSECURE_ALLOW_ALL
from adcp.server.test_controller import TestControllerError, TestControllerStore

PORT = int(os.environ.get("ADCP_PORT") or os.environ.get("PORT") or 3001)
Expand Down Expand Up @@ -168,6 +168,46 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
# Tasks registered when create_media_buy consumes a 'submitted' directive; keyed by task_id.
pending_task_completions: dict[str, dict[str, Any]] = {}


def _image_format_options(
*,
capability_id: str,
display_name: str,
v1_format_id: str,
width: int,
height: int,
) -> list[dict[str, Any]]:
"""Build a v2 ``format_options[]`` entry pointing back at a v1 ``format_id``.

Dual-emit pattern: this reference seller publishes the v1
``Product.format_ids[]`` for 3.0 buyers and the v2
``Product.format_options[]`` for 3.1 buyers. The two carry the
same underlying format; the v2 declaration's ``v1_format_ref``
asserts the pairing so SDKs running the v2 → v1 projection (see
``adcp.canonical_formats.project_product_to_v1``) round-trip
format_ids back to the v1 emit.

Adopters reading this file as a template SHOULD prefer
publishing both shapes for the duration of the 3.0 → 3.1
migration window; the storyboard runner exercises both paths
against this reference.
"""
return [
{
"format_kind": "image",
"capability_id": capability_id,
"display_name": display_name,
"v1_format_ref": [{"agent_url": AGENT_URL, "id": v1_format_id}],
"params": {
"sizes": [{"width": width, "height": height}],
"asset_source": "buyer_uploaded",
"ssl_required": True,
"image_formats": ["jpg", "png", "gif"],
},
}
]


PRODUCTS: list[dict[str, Any]] = [
{
"product_id": "premium-homepage",
Expand All @@ -176,6 +216,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_970x250"}],
"format_options": _image_format_options(
capability_id="example_billboard_970x250",
display_name="Example.com Homepage — Billboard",
v1_format_id="display_970x250",
width=970,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "po-cpm-homepage",
Expand All @@ -201,6 +248,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "non_guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
"format_options": _image_format_options(
capability_id="example_mrec_300x250",
display_name="Example.com RoS — MREC",
v1_format_id="display_300x250",
width=300,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "po-cpm-ros",
Expand Down Expand Up @@ -229,6 +283,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "non_guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
"format_options": _image_format_options(
capability_id="storyboard_outdoor_display_300x250",
display_name="Outdoor Display Q2 — MREC",
v1_format_id="display_300x250",
width=300,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "cpm_standard",
Expand All @@ -254,6 +315,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "non_guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
"format_options": _image_format_options(
capability_id="storyboard_outdoor_video_300x250",
display_name="Outdoor Video Q2 — MREC fallback",
v1_format_id="display_300x250",
width=300,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "cpm_standard",
Expand All @@ -279,6 +347,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_970x250"}],
"format_options": _image_format_options(
capability_id="storyboard_sports_preroll_970x250",
display_name="Sports Preroll Q2 — Billboard",
v1_format_id="display_970x250",
width=970,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "cpm_guaranteed",
Expand All @@ -304,6 +379,13 @@ def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -
"delivery_type": "non_guaranteed",
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
"format_options": _image_format_options(
capability_id="storyboard_lifestyle_display_300x250",
display_name="Lifestyle Display Q2 — MREC",
v1_format_id="display_300x250",
width=300,
height=250,
),
"pricing_options": [
{
"pricing_option_id": "cpm_standard",
Expand Down
108 changes: 108 additions & 0 deletions tests/test_seller_agent_dual_emit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""``examples/seller_agent.py`` dual-emits v1 + v2 format surfaces.

The reference seller is the AdCP storyboard runner's target — it MUST
exercise both wire shapes so the storyboard suite catches regressions
on either path. This test pins that invariant.

Three properties are pinned:

* Every product carries both ``format_ids[]`` (v1) and
``format_options[]`` (v2).
* Each v2 declaration's ``v1_format_ref[0]`` matches the product's
v1 ``format_ids[0]`` — the seller-asserted pairing is the round-
trip anchor.
* :func:`project_product_to_v1` on each product emits exactly the
product's v1 ``format_ids`` (the SDK's projection layer rebuilds
what the seller manually authored on v1).
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path
from typing import Any

import pytest

from adcp.canonical_formats import (
find_declaration_by_v1_format_id,
project_product_to_v1,
)
from adcp.types import ProductFormatDeclaration

_SELLER_AGENT_PATH = Path(__file__).parent.parent / "examples" / "seller_agent.py"


@pytest.fixture(scope="module")
def seller_agent_products() -> list[dict[str, Any]]:
"""Import the reference seller's ``PRODUCTS`` catalog as a fixture.

The seller agent isn't on ``sys.path`` by default; load via
``importlib`` rather than rewriting ``sys.path`` so the import
is hermetic and the test doesn't leak state.
"""
spec = importlib.util.spec_from_file_location("seller_agent", _SELLER_AGENT_PATH)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules["seller_agent"] = module
spec.loader.exec_module(module)
try:
return list(module.PRODUCTS)
finally:
sys.modules.pop("seller_agent", None)


def test_every_product_dual_emits_v1_and_v2(
seller_agent_products: list[dict[str, Any]],
) -> None:
for p in seller_agent_products:
assert p.get("format_ids"), f"{p['product_id']}: missing v1 format_ids"
assert p.get("format_options"), f"{p['product_id']}: missing v2 format_options"


def test_v2_declarations_pair_with_v1_format_ids(
seller_agent_products: list[dict[str, Any]],
) -> None:
"""The v2 declaration MUST carry ``v1_format_ref`` pointing at the same
underlying format the product publishes on v1."""
for p in seller_agent_products:
v1_ids = {fmt["id"] for fmt in p["format_ids"]}
v2_ref_ids: set[str] = set()
for decl in p["format_options"]:
for ref in decl.get("v1_format_ref") or []:
v2_ref_ids.add(ref["id"])
assert v1_ids == v2_ref_ids, (
f"{p['product_id']}: v1 format_ids {v1_ids!r} disagree with "
f"v2 v1_format_ref entries {v2_ref_ids!r}"
)


def test_v2_to_v1_projection_round_trips_each_product(
seller_agent_products: list[dict[str, Any]],
) -> None:
"""Running :func:`project_product_to_v1` over each product's
typed declarations MUST emit refs that round-trip back to those
declarations via :func:`find_declaration_by_v1_format_id`, and
MUST NOT raise any unexpected advisories.
"""
for p in seller_agent_products:
declarations = [ProductFormatDeclaration.model_validate(opt) for opt in p["format_options"]]

class _Product:
product_id = p["product_id"]
format_options = declarations

result = project_product_to_v1(_Product(), product_index=0)
assert result.format_ids, f"{p['product_id']}: emitted zero v1 refs"
for ref in result.format_ids:
found = find_declaration_by_v1_format_id(ref, declarations)
assert found is not None, (
f"{p['product_id']}: emitted format_id {ref.id!r} did not "
f"resolve back to any declaration"
)
# The reference seller's products are single-size; no LOSSY or
# AMBIGUOUS advisories should fire.
assert (
result.advisories == []
), f"{p['product_id']}: unexpected advisories {[a.code for a in result.advisories]!r}"
Loading