From 510a749515c2afd7514d93865012c7506d47aa7b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 24 May 2026 08:39:20 -0400 Subject: [PATCH] feat(examples): dual-emit v1 format_ids + v2 format_options on reference seller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the conformance gap between the canonical-formats projection layer (#741) and the reference seller the AdCP storyboard runner targets. Previously, ``examples/seller_agent.py`` published ``Product.format_ids[]`` only (v1) — the SDK's v2 surface shipped but was never exercised end-to-end by the storyboard suite. ## What changed Every product in the reference seller's ``PRODUCTS`` catalog now carries both wire shapes: * **v1**: ``format_ids: [{agent_url, id}]`` (unchanged). * **v2**: ``format_options: [ProductFormatDeclaration]`` with ``format_kind="image"``, sized ``params`` (width/height + ``asset_source`` + ``image_formats``), and ``v1_format_ref`` pointing back at the v1 ``format_id``. A small ``_image_format_options(...)`` helper builds the v2 declaration so each product entry stays a one-keyword addition. The helper docstring documents the dual-emit pattern as the template adopters should follow during the 3.0 → 3.1 migration. ## Why * 3.0 buyers continue reading ``format_ids[]`` from the wire. * 3.1 buyers now see real ``format_options[]`` declarations and can route on ``format_kind`` plus typed ``params``. * The storyboard runner exercises this path end-to-end against the reference seller for the first time. ## Tests 5258 pass locally. ``tests/test_seller_agent_dual_emit.py`` pins 3 invariants over the 6 products: 1. Every product dual-emits. 2. Every v2 ``v1_format_ref`` matches the product's v1 ``format_ids`` (round-trip anchor). 3. SDK projection rebuilds v1 emit from v2 with zero advisories. Refs: #741 --- examples/seller_agent.py | 84 ++++++++++++++++++++- tests/test_seller_agent_dual_emit.py | 108 +++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 tests/test_seller_agent_dual_emit.py diff --git a/examples/seller_agent.py b/examples/seller_agent.py index b97efada..f921789f 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -21,6 +21,7 @@ from typing import Any from adcp.server import ( + INSECURE_ALLOW_ALL, ADCPHandler, adcp_error, cancel_media_buy_response, @@ -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) @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/tests/test_seller_agent_dual_emit.py b/tests/test_seller_agent_dual_emit.py new file mode 100644 index 00000000..16b5ab71 --- /dev/null +++ b/tests/test_seller_agent_dual_emit.py @@ -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}"