Skip to content

Commit d92cfb1

Browse files
bokelleyclaude
andauthored
fix(examples): close last 5 storyboard fixture-dependent failures (#319) (#322)
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) <noreply@anthropic.com>
1 parent e37930a commit d92cfb1

1 file changed

Lines changed: 161 additions & 11 deletions

File tree

examples/seller_agent.py

Lines changed: 161 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,109 @@
112112
},
113113
"delivery_measurement": {"provider": "internal"},
114114
},
115+
# Storyboard test fixtures referenced by @adcp/client compliance YAMLs.
116+
# The runner's media_buy_seller suite expects these product IDs to be
117+
# discoverable without an explicit seed_product call.
118+
{
119+
"product_id": "outdoor_display_q2",
120+
"name": "Outdoor Display Q2",
121+
"description": "Outdoor display inventory for Q2 storyboards",
122+
"delivery_type": "non_guaranteed",
123+
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
124+
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
125+
"pricing_options": [
126+
{
127+
"pricing_option_id": "cpm_standard",
128+
"pricing_model": "cpm",
129+
"floor_price": 5.00,
130+
"currency": "USD",
131+
}
132+
],
133+
"reporting_capabilities": {
134+
"available_metrics": ["impressions", "spend", "clicks", "ctr"],
135+
"available_reporting_frequencies": ["hourly", "daily"],
136+
"date_range_support": "date_range",
137+
"supports_webhooks": False,
138+
"expected_delay_minutes": 60,
139+
"timezone": "UTC",
140+
},
141+
"delivery_measurement": {"provider": "internal"},
142+
},
143+
{
144+
"product_id": "outdoor_video_q2",
145+
"name": "Outdoor Video Q2",
146+
"description": "Outdoor video inventory for Q2 storyboards",
147+
"delivery_type": "non_guaranteed",
148+
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
149+
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
150+
"pricing_options": [
151+
{
152+
"pricing_option_id": "cpm_standard",
153+
"pricing_model": "cpm",
154+
"floor_price": 8.00,
155+
"currency": "USD",
156+
}
157+
],
158+
"reporting_capabilities": {
159+
"available_metrics": ["impressions", "spend", "clicks", "ctr"],
160+
"available_reporting_frequencies": ["hourly", "daily"],
161+
"date_range_support": "date_range",
162+
"supports_webhooks": False,
163+
"expected_delay_minutes": 60,
164+
"timezone": "UTC",
165+
},
166+
"delivery_measurement": {"provider": "internal"},
167+
},
168+
{
169+
"product_id": "sports_preroll_q2",
170+
"name": "Sports Preroll Q2",
171+
"description": "Sports preroll video inventory for Q2 storyboards",
172+
"delivery_type": "guaranteed",
173+
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
174+
"format_ids": [{"agent_url": AGENT_URL, "id": "display_970x250"}],
175+
"pricing_options": [
176+
{
177+
"pricing_option_id": "cpm_guaranteed",
178+
"pricing_model": "cpm",
179+
"floor_price": 25.00,
180+
"currency": "USD",
181+
}
182+
],
183+
"reporting_capabilities": {
184+
"available_metrics": ["impressions", "spend", "clicks", "ctr"],
185+
"available_reporting_frequencies": ["hourly", "daily"],
186+
"date_range_support": "date_range",
187+
"supports_webhooks": False,
188+
"expected_delay_minutes": 60,
189+
"timezone": "UTC",
190+
},
191+
"delivery_measurement": {"provider": "internal"},
192+
},
193+
{
194+
"product_id": "lifestyle_display_q2",
195+
"name": "Lifestyle Display Q2",
196+
"description": "Lifestyle display inventory for Q2 storyboards",
197+
"delivery_type": "non_guaranteed",
198+
"publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}],
199+
"format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}],
200+
"pricing_options": [
201+
{
202+
"pricing_option_id": "cpm_standard",
203+
"pricing_model": "cpm",
204+
"floor_price": 6.00,
205+
"currency": "USD",
206+
}
207+
],
208+
"reporting_capabilities": {
209+
"available_metrics": ["impressions", "spend", "clicks", "ctr"],
210+
"available_reporting_frequencies": ["hourly", "daily"],
211+
"date_range_support": "date_range",
212+
"supports_webhooks": False,
213+
"expected_delay_minutes": 60,
214+
"timezone": "UTC",
215+
},
216+
"delivery_measurement": {"provider": "internal"},
217+
},
115218
]
116219

117220

@@ -262,14 +365,42 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) ->
262365
field="product_id",
263366
suggestion="Use get_products to discover available products",
264367
)
265-
packages.append(
266-
{
267-
"package_id": f"pkg-{uuid.uuid4().hex[:8]}",
268-
"product_id": product_id,
269-
"pricing_option_id": pkg.get("pricing_option_id"),
270-
"budget": pkg.get("budget"),
271-
}
272-
)
368+
# Reject aggressive measurement_terms. The compliance runner sends
369+
# max_variance_percent=0 with a c30 window (unworkable) on the
370+
# rejection path, then retries with c7 + 10% variance.
371+
terms = pkg.get("measurement_terms") or {}
372+
billing = terms.get("billing_measurement") or {}
373+
window = billing.get("measurement_window")
374+
variance = billing.get("max_variance_percent")
375+
if (variance is not None and variance < 5) or (
376+
window is not None and window not in ("c3", "c7")
377+
):
378+
return adcp_error(
379+
"TERMS_REJECTED",
380+
"Measurement terms unworkable: variance must be >=5%, "
381+
"measurement_window must be c3 or c7.",
382+
field="measurement_terms",
383+
recovery="correctable",
384+
)
385+
built_pkg: dict[str, Any] = {
386+
"package_id": f"pkg-{uuid.uuid4().hex[:8]}",
387+
"product_id": product_id,
388+
"pricing_option_id": pkg.get("pricing_option_id"),
389+
"budget": pkg.get("budget"),
390+
}
391+
# Persist caller-supplied package fields the runner expects to
392+
# round-trip on get_media_buys (targeting_overlay) or to drive
393+
# status transitions (creative_assignments, creatives,
394+
# measurement_terms).
395+
for field in (
396+
"targeting_overlay",
397+
"creative_assignments",
398+
"creatives",
399+
"measurement_terms",
400+
):
401+
if pkg.get(field) is not None:
402+
built_pkg[field] = pkg[field]
403+
packages.append(built_pkg)
273404

274405
has_creatives = any(
275406
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) ->
320451
return adcp_error("CONFLICT", "Revision mismatch - refetch and retry")
321452

322453
if params.get("packages"):
323-
existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])}
454+
existing_by_id = {p["package_id"]: p for p in mb.get("packages", [])}
324455
for pkg_update in params["packages"]:
325456
pkg_id = pkg_update.get("package_id")
326-
if pkg_id and pkg_id not in existing_pkg_ids:
457+
if pkg_id and pkg_id not in existing_by_id:
327458
return adcp_error(
328459
"PACKAGE_NOT_FOUND",
329460
f"Package '{pkg_id}' not found in media buy {mb_id}",
330461
field="package_id",
331462
)
463+
# Apply incoming targeting/budget/creative deltas to the
464+
# persisted package so a subsequent get_media_buys reflects
465+
# the change. Storyboard inventory_list_targeting/update
466+
# asserts targeting_overlay round-trips through this path.
467+
if pkg_id and pkg_id in existing_by_id:
468+
target = existing_by_id[pkg_id]
469+
for field in (
470+
"targeting_overlay",
471+
"creative_assignments",
472+
"creatives",
473+
"measurement_terms",
474+
"budget",
475+
):
476+
if pkg_update.get(field) is not None:
477+
target[field] = pkg_update[field]
332478

333479
status = mb["status"]
334480
if status == "pending_creatives" and params.get("packages"):
@@ -699,7 +845,11 @@ async def seed_creative_format(
699845
context: Any = None,
700846
) -> dict[str, Any]:
701847
data = dict(fixture or {})
702-
fid = format_id or (data.get("format_id") or {}).get("id") or f"fmt-seeded-{uuid.uuid4().hex[:8]}"
848+
fid = (
849+
format_id
850+
or (data.get("format_id") or {}).get("id")
851+
or f"fmt-seeded-{uuid.uuid4().hex[:8]}"
852+
)
703853
data.setdefault("format_id", {"agent_url": AGENT_URL, "id": fid})
704854
data.setdefault("name", fid)
705855
data.setdefault("renders", [])

0 commit comments

Comments
 (0)