diff --git a/skills/build-seller-agent/SKILL.md b/skills/build-seller-agent/SKILL.md index 2e1b0328..252b273d 100644 --- a/skills/build-seller-agent/SKILL.md +++ b/skills/build-seller-agent/SKILL.md @@ -168,7 +168,7 @@ class MySeller(ADCPHandler): Sellers emit webhooks to notify buyers asynchronously. AdCP 3.0 uses RFC 9421 HTTP Message Signatures — use `adcp.webhooks.WebhookSender` (one-call helper) or `sign_webhook` for lower-level control. **When to emit:** -- Async-approval media buy transitions (`pending_activation` → `active`, or `→ rejected`) +- Async-approval media buy transitions (`pending_creatives` → `pending_start` → `active`, or `→ rejected`) - Artifact-ready notifications after long-running operations - Delivery reports the buyer subscribed to via `reporting_webhook` on `create_media_buy` diff --git a/src/adcp/server/helpers.py b/src/adcp/server/helpers.py index 0e6f80f1..0ffda133 100644 --- a/src/adcp/server/helpers.py +++ b/src/adcp/server/helpers.py @@ -141,7 +141,15 @@ def adcp_error( # Actions are operations available via update_media_buy for each status. # Public constant — servers can inspect, test against, or extend this. MEDIA_BUY_STATE_MACHINE: dict[str, list[str]] = { - "pending_activation": [ + "pending_creatives": [ + "cancel", + "update_budget", + "update_dates", + "update_packages", + "add_packages", + "sync_creatives", + ], + "pending_start": [ "cancel", "update_budget", "update_dates", @@ -164,7 +172,18 @@ def adcp_error( def valid_actions_for_status(status: str) -> list[str]: - """Get valid buyer actions for a media buy status.""" + """Get valid buyer actions for a media buy status. + + Returns the list of ``update_media_buy`` actions available to a buyer for + the given status string. Returns ``[]`` for terminal statuses and for any + unrecognized status string. + + Valid statuses per ``enums/media-buy-status.json``: + ``pending_creatives``, ``pending_start``, ``active``, ``paused``, + ``completed``, ``rejected``, ``canceled``. + + Inspect or extend :data:`MEDIA_BUY_STATE_MACHINE` to add custom actions. + """ return list(MEDIA_BUY_STATE_MACHINE.get(status, [])) diff --git a/tests/test_server_helpers.py b/tests/test_server_helpers.py index 0df53a3a..5947acfd 100644 --- a/tests/test_server_helpers.py +++ b/tests/test_server_helpers.py @@ -85,11 +85,41 @@ def test_terminal_statuses_empty(self) -> None: for status in ("completed", "rejected", "canceled"): assert valid_actions_for_status(status) == [] - def test_pending_activation_allows_cancel(self) -> None: - actions = valid_actions_for_status("pending_activation") + def test_pending_start_allows_cancel(self) -> None: + actions = valid_actions_for_status("pending_start") assert "cancel" in actions assert "update_packages" in actions + def test_pending_creatives_allows_sync_creatives(self) -> None: + actions = valid_actions_for_status("pending_creatives") + assert "sync_creatives" in actions + assert "cancel" in actions + + def test_pending_activation_is_not_recognized(self) -> None: + # AdCP v3 renamed pending_activation to pending_creatives + pending_start. + # Lock the rename so a copy-paste from old docs / old SDK code returns + # an empty action list rather than silently matching a stale entry. + from adcp.server.helpers import MEDIA_BUY_STATE_MACHINE + + assert valid_actions_for_status("pending_activation") == [] + assert "pending_activation" not in MEDIA_BUY_STATE_MACHINE + + def test_state_machine_keys_match_spec_enum(self) -> None: + # Keys must exactly match enums/media-buy-status.json. Terminal + # statuses are present with empty action lists. Guards against + # future silent drift between the spec enum and the dispatcher. + from adcp.server.helpers import MEDIA_BUY_STATE_MACHINE + + assert set(MEDIA_BUY_STATE_MACHINE) == { + "pending_creatives", + "pending_start", + "active", + "paused", + "completed", + "rejected", + "canceled", + } + def test_unknown_status_empty(self) -> None: assert valid_actions_for_status("nonexistent") == []