From add682202923d509a526985d391f52fc7668ad9f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 30 Apr 2026 10:09:23 -0400 Subject: [PATCH] fix(examples): close last 5 storyboard fixture-dependent failures (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end against npx @adcp/client adcp storyboard run now reports overall_status: passing with 47/47 individual steps passing and controller_detected: true (was 36/47 partial after #317). Three fixes against examples/seller_agent.py: 1. Add four runner-fixture products to PRODUCTS — outdoor_display_q2, outdoor_video_q2, sports_preroll_q2, lifestyle_display_q2. The @adcp/client storyboard YAMLs reference these by ID without an explicit seed_product setup step; the seller is expected to know them out of the box. Pricing option IDs (cpm_standard, cpm_guaranteed) match what the compliance YAMLs send. 2. Validate measurement_terms in create_media_buy — reject with TERMS_REJECTED when max_variance_percent < 5 or measurement_window is not in (c3, c7). Source-of-truth is the measurement_terms_rejected.yaml storyboard which probes with c30 + 0% (rejected path) then retries with c7 + 10% (accepted path). Acceptance threshold matches the runner's relaxed shape. 3. Persist targeting_overlay (and friends) on packages — both create_media_buy and update_media_buy now preserve targeting_overlay, creative_assignments, creatives, and measurement_terms on the persisted package state, then surface them on get_media_buys. Storyboard inventory_list_targeting/verify_create_persisted round-trips property_list.list_id; the swap variant exercises the update parity check. Once this lands, the storyboard CI job (#309, currently continue-on-error: true) is promotable to required. Closes #319 Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/seller_agent.py | 172 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 11 deletions(-) diff --git a/examples/seller_agent.py b/examples/seller_agent.py index 63c93668..30ebd4a9 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -112,6 +112,109 @@ }, "delivery_measurement": {"provider": "internal"}, }, + # Storyboard test fixtures referenced by @adcp/client compliance YAMLs. + # The runner's media_buy_seller suite expects these product IDs to be + # discoverable without an explicit seed_product call. + { + "product_id": "outdoor_display_q2", + "name": "Outdoor Display Q2", + "description": "Outdoor display inventory for Q2 storyboards", + "delivery_type": "non_guaranteed", + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], + "pricing_options": [ + { + "pricing_option_id": "cpm_standard", + "pricing_model": "cpm", + "floor_price": 5.00, + "currency": "USD", + } + ], + "reporting_capabilities": { + "available_metrics": ["impressions", "spend", "clicks", "ctr"], + "available_reporting_frequencies": ["hourly", "daily"], + "date_range_support": "date_range", + "supports_webhooks": False, + "expected_delay_minutes": 60, + "timezone": "UTC", + }, + "delivery_measurement": {"provider": "internal"}, + }, + { + "product_id": "outdoor_video_q2", + "name": "Outdoor Video Q2", + "description": "Outdoor video inventory for Q2 storyboards", + "delivery_type": "non_guaranteed", + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], + "pricing_options": [ + { + "pricing_option_id": "cpm_standard", + "pricing_model": "cpm", + "floor_price": 8.00, + "currency": "USD", + } + ], + "reporting_capabilities": { + "available_metrics": ["impressions", "spend", "clicks", "ctr"], + "available_reporting_frequencies": ["hourly", "daily"], + "date_range_support": "date_range", + "supports_webhooks": False, + "expected_delay_minutes": 60, + "timezone": "UTC", + }, + "delivery_measurement": {"provider": "internal"}, + }, + { + "product_id": "sports_preroll_q2", + "name": "Sports Preroll Q2", + "description": "Sports preroll video inventory for Q2 storyboards", + "delivery_type": "guaranteed", + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], + "format_ids": [{"agent_url": AGENT_URL, "id": "display_970x250"}], + "pricing_options": [ + { + "pricing_option_id": "cpm_guaranteed", + "pricing_model": "cpm", + "floor_price": 25.00, + "currency": "USD", + } + ], + "reporting_capabilities": { + "available_metrics": ["impressions", "spend", "clicks", "ctr"], + "available_reporting_frequencies": ["hourly", "daily"], + "date_range_support": "date_range", + "supports_webhooks": False, + "expected_delay_minutes": 60, + "timezone": "UTC", + }, + "delivery_measurement": {"provider": "internal"}, + }, + { + "product_id": "lifestyle_display_q2", + "name": "Lifestyle Display Q2", + "description": "Lifestyle display inventory for Q2 storyboards", + "delivery_type": "non_guaranteed", + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], + "pricing_options": [ + { + "pricing_option_id": "cpm_standard", + "pricing_model": "cpm", + "floor_price": 6.00, + "currency": "USD", + } + ], + "reporting_capabilities": { + "available_metrics": ["impressions", "spend", "clicks", "ctr"], + "available_reporting_frequencies": ["hourly", "daily"], + "date_range_support": "date_range", + "supports_webhooks": False, + "expected_delay_minutes": 60, + "timezone": "UTC", + }, + "delivery_measurement": {"provider": "internal"}, + }, ] @@ -262,14 +365,42 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> field="product_id", suggestion="Use get_products to discover available products", ) - packages.append( - { - "package_id": f"pkg-{uuid.uuid4().hex[:8]}", - "product_id": product_id, - "pricing_option_id": pkg.get("pricing_option_id"), - "budget": pkg.get("budget"), - } - ) + # Reject aggressive measurement_terms. The compliance runner sends + # max_variance_percent=0 with a c30 window (unworkable) on the + # rejection path, then retries with c7 + 10% variance. + terms = pkg.get("measurement_terms") or {} + billing = terms.get("billing_measurement") or {} + window = billing.get("measurement_window") + variance = billing.get("max_variance_percent") + if (variance is not None and variance < 5) or ( + window is not None and window not in ("c3", "c7") + ): + return adcp_error( + "TERMS_REJECTED", + "Measurement terms unworkable: variance must be >=5%, " + "measurement_window must be c3 or c7.", + field="measurement_terms", + recovery="correctable", + ) + built_pkg: dict[str, Any] = { + "package_id": f"pkg-{uuid.uuid4().hex[:8]}", + "product_id": product_id, + "pricing_option_id": pkg.get("pricing_option_id"), + "budget": pkg.get("budget"), + } + # Persist caller-supplied package fields the runner expects to + # round-trip on get_media_buys (targeting_overlay) or to drive + # status transitions (creative_assignments, creatives, + # measurement_terms). + for field in ( + "targeting_overlay", + "creative_assignments", + "creatives", + "measurement_terms", + ): + if pkg.get(field) is not None: + built_pkg[field] = pkg[field] + packages.append(built_pkg) has_creatives = any( pkg.get("creative_assignments") or pkg.get("creatives") for pkg in params["packages"] @@ -320,15 +451,30 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> return adcp_error("CONFLICT", "Revision mismatch - refetch and retry") if params.get("packages"): - existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])} + existing_by_id = {p["package_id"]: p for p in mb.get("packages", [])} for pkg_update in params["packages"]: pkg_id = pkg_update.get("package_id") - if pkg_id and pkg_id not in existing_pkg_ids: + if pkg_id and pkg_id not in existing_by_id: return adcp_error( "PACKAGE_NOT_FOUND", f"Package '{pkg_id}' not found in media buy {mb_id}", field="package_id", ) + # Apply incoming targeting/budget/creative deltas to the + # persisted package so a subsequent get_media_buys reflects + # the change. Storyboard inventory_list_targeting/update + # asserts targeting_overlay round-trips through this path. + if pkg_id and pkg_id in existing_by_id: + target = existing_by_id[pkg_id] + for field in ( + "targeting_overlay", + "creative_assignments", + "creatives", + "measurement_terms", + "budget", + ): + if pkg_update.get(field) is not None: + target[field] = pkg_update[field] status = mb["status"] if status == "pending_creatives" and params.get("packages"): @@ -699,7 +845,11 @@ async def seed_creative_format( context: Any = None, ) -> dict[str, Any]: data = dict(fixture or {}) - fid = format_id or (data.get("format_id") or {}).get("id") or f"fmt-seeded-{uuid.uuid4().hex[:8]}" + fid = ( + format_id + or (data.get("format_id") or {}).get("id") + or f"fmt-seeded-{uuid.uuid4().hex[:8]}" + ) data.setdefault("format_id", {"agent_url": AGENT_URL, "id": fid}) data.setdefault("name", fid) data.setdefault("renders", [])