Skip to content

feat(server): add force_create_media_buy_arm + force_task_completion controller scenarios#282

Merged
bokelley merged 3 commits intomainfrom
claude/issue-281-force-create-media-buy-arm-force-task-completion
Apr 30, 2026
Merged

feat(server): add force_create_media_buy_arm + force_task_completion controller scenarios#282
bokelley merged 3 commits intomainfrom
claude/issue-281-force-create-media-buy-arm-force-task-completion

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Apr 25, 2026

Closes #281

Rebased onto post-#292 main (AdCP 3.0.1). The schema-cache and generated-type edits from the first pass are dropped — #292 already ships them via make regenerate-schemas. The dispatcher logic, tests, and schema derivation are the only changes in this PR.

Adds two new TestControllerStore scenarios for storyboard parity with the Node training-agent (adcp#3115, adcp#3194). Sellers running the create_media_buy_async.yaml storyboard suite against a Python reference seller now grade passing rather than not_applicable on the submitted-arm phase.

Changes

src/adcp/server/test_controller.py

  • Adds force_create_media_buy_arm and force_task_completion to SCENARIOS.
  • Adds abstract base methods with full docstrings (idempotency/isolation contract, account param for cross-account isolation).
  • Adds dispatcher branches with input validation: arm enum, conditional task_id when arm='submitted', task_id stripped to None for arm='input-required' (Forced.extra="forbid" in response schema), char limits (128/2000), non-empty result dict, 256 KB size cap.
  • Extracts _accepts_kwarg(method, name) so both context and account pass-through share one signature-inspection impl; gates account via the same opt-in pattern as context.
  • Derives register_test_controller inline schema from SCENARIOS to prevent drift; adds account field.

src/adcp/server/mcp_tools.py

  • Imports SCENARIOS from test_controller and derives the comply_test_controller inputSchema enum from it (no more hardcoded list); adds account field.

tests/test_force_create_media_buy_arm_and_force_task_completion.py

  • 21 new tests: valid registration (submitted + input-required arms), overwrite-pending, task_id stripped for input-required, all INVALID_PARAMS branches (missing/bad arm, task_id conditional, char limits, whitespace task_id, empty result, result too large), force_task_completion valid/idempotent/diverging-INVALID_TRANSITION/cross-account NOT_FOUND, list_scenarios advertisement (both/neither/partial).

What was tested

  • pytest tests/ (excluding slow integration + ip_pinned_transport): 2216 passed, 21 skipped, 0 failures
  • ruff check src/adcp/server/test_controller.py src/adcp/server/mcp_tools.py: clean
  • mypy src/adcp/server/test_controller.py: no new errors (pre-existing import-not-found from mypy env; same count as before)

Pre-PR review

  • code-reviewer: approved — account kwarg now gated via _accepts_kwarg (consistent with context); mcp_tools.py derives enum from SCENARIOS; message now uses isinstance guard; 2 nits noted below
  • ad-tech-protocol-expert: approved — arm enum correct; NOT_FOUND for cross-account is correct privacy-preserving choice; INVALID_TRANSITION + current_state="completed" matches ComplyTestControllerResponse6 exactly; 256 KB cap is stricter-but-conforming; blocker (task_id pass-through to input-required arm) fixed

Nits surfaced (not fixed — follow-up candidates)

  • Non-string task_id (integer from malformed JSON) hits TypeError in len(), landing in INTERNAL_ERROR instead of INVALID_PARAMS — pre-existing pattern in other dispatchers; low-severity since MCP clients are typed
  • _BothStore in tests has a redundant explicit TestControllerStore base (already inherited via both parents)

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01KaGEJKsjnTEuLF6qnaRFqQ


Generated by Claude Code

@bokelley bokelley force-pushed the claude/issue-281-force-create-media-buy-arm-force-task-completion branch from 5e317d1 to 8e8af0e Compare April 29, 2026 23:24
@bokelley bokelley changed the title feat(adcp): add force_create_media_buy_arm + force_task_completion controller scenarios feat(server): add force_create_media_buy_arm + force_task_completion controller scenarios Apr 29, 2026
bokelley pushed a commit that referenced this pull request Apr 30, 2026
…e (items 1–6 of #304)

Six gaps identified by the media_buy_seller storyboard runner after the #296
transport fix exposed content-side failures in the reference example:

1. declare `adcp.idempotency` in capabilities so the runner does not downgrade
   to v2 mode (`idempotency={"supported": False}`)
2. include `total_budget` (schema-required number) in `get_media_buys` entries,
   computed as the sum of per-package budgets
3. return `status=pending_creatives` from `create_media_buy` when no
   `creative_assignments`/`creatives` are in the request packages, and
   transition to `active` in `update_media_buy` when creatives are attached
4. fix `list_creative_formats` render shape: wrap width/height in a
   `dimensions` object and add the required `role` field
5. honour the `format_ids` filter in `list_creative_formats`, matching on the
   full `(agent_url, id)` pair
6. return `PACKAGE_NOT_FOUND` in `update_media_buy` when a package ID in the
   update request does not exist in the stored media buy

Item 7 (seed_product / controller_detected) remains blocked on #282.

https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2
bokelley pushed a commit that referenced this pull request Apr 30, 2026
…e (items 1–6 of #304)

Six gaps identified by the media_buy_seller storyboard runner after the #296
transport fix exposed content-side failures in the reference example:

1. declare `adcp.idempotency` in capabilities so the runner does not downgrade
   to v2 mode (`idempotency={"supported": False}`)
2. include `total_budget` (schema-required number) in `get_media_buys` entries,
   computed as the sum of per-package budgets
3. return `status=pending_creatives` from `create_media_buy` when no
   `creative_assignments`/`creatives` are in the request packages, and
   transition to `active` in `update_media_buy` when creatives are attached
4. fix `list_creative_formats` render shape: wrap width/height in a
   `dimensions` object and add the required `role` field
5. honour the `format_ids` filter in `list_creative_formats`, matching on the
   full `(agent_url, id)` pair
6. return `PACKAGE_NOT_FOUND` in `update_media_buy` when a package ID in the
   update request does not exist in the stored media buy

Item 7 (seed_product / controller_detected) remains blocked on #282.

https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2
bokelley added a commit that referenced this pull request Apr 30, 2026
…s 1-6 of #304) (#310)

* fix(examples): seller_agent.py passes AdCP 3.0.1 storyboard compliance (items 1–6 of #304)

Six gaps identified by the media_buy_seller storyboard runner after the #296
transport fix exposed content-side failures in the reference example:

1. declare `adcp.idempotency` in capabilities so the runner does not downgrade
   to v2 mode (`idempotency={"supported": False}`)
2. include `total_budget` (schema-required number) in `get_media_buys` entries,
   computed as the sum of per-package budgets
3. return `status=pending_creatives` from `create_media_buy` when no
   `creative_assignments`/`creatives` are in the request packages, and
   transition to `active` in `update_media_buy` when creatives are attached
4. fix `list_creative_formats` render shape: wrap width/height in a
   `dimensions` object and add the required `role` field
5. honour the `format_ids` filter in `list_creative_formats`, matching on the
   full `(agent_url, id)` pair
6. return `PACKAGE_NOT_FOUND` in `update_media_buy` when a package ID in the
   update request does not exist in the stored media buy

Item 7 (seed_product / controller_detected) remains blocked on #282.

https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2

* fix(examples): align DemoStore.simulate_delivery reported_spend type with base class

The base TestControllerStore declares reported_spend as dict[str, Any] | None
(matching the ReportedSpend schema {amount, currency}). DemoStore had it as
float | None, causing type mismatch and incorrect stored structure when the
storyboard sends a structured object.

https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2

* fix(examples): explicitly pass valid_actions for pending_creatives status

MEDIA_BUY_STATE_MACHINE on main lacks the pending_creatives key (it lands with
PR #296). Without explicit valid_actions, media_buy_response() and
update_media_buy_response() return valid_actions=[] for pending_creatives buys,
blocking the storyboard from discovering that sync_creatives is available.

Pass the expected actions list explicitly until #296 merges.

https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2

---------

Co-authored-by: Claude <noreply@anthropic.com>
claude added 3 commits April 29, 2026 21:19
…controller scenarios

Adds two new comply_test_controller scenarios for AdCP 3.0.1 storyboard
parity. Sellers running the create_media_buy_async.yaml storyboard suite
against a Python reference seller now grade `passing` rather than
`not_applicable` on the submitted-arm phase.

- Adds `force_create_media_buy_arm` and `force_task_completion` to SCENARIOS,
  TestControllerStore abstract base, and the dispatcher in _handle_test_controller.
- Validates arm enum, conditional task_id-when-submitted, char limits, 256 KB
  result cap, and whitespace task_id stripping.
- Updates register_test_controller inline schema (derived from SCENARIOS to
  prevent drift) and mcp_tools.py ADCP_TOOL_DEFINITIONS enum to include both.
- Adds account field to both inline schemas so storyboard runners can drive
  cross-account isolation.
- 20 new tests at parity with Node training-agent nine-test pattern.

Closes #281

https://claude.ai/code/session_01KaGEJKsjnTEuLF6qnaRFqQ
Forced.extra='forbid' in the comply_test_controller response schema means
a store that echoes task_id on arm='input-required' would produce an
invalid Forced object. The dispatcher now nullifies task_id before the
store call when arm='input-required', preventing protocol drift regardless
of store implementation.

Adds one test: test_arm_task_id_stripped_for_input_required.

https://claude.ai/code/session_01KaGEJKsjnTEuLF6qnaRFqQ
- Extract _accepts_kwarg(method, name) so both context and account
  pass-through share one signature-inspection impl; _accepts_context_kwarg
  delegates to it.
- Gate account kwarg via _accepts_kwarg in the shared `extra` dict so
  stores that omit account= don't receive an unexpected keyword and
  silently fall to INTERNAL_ERROR.
- Replace len(str(message)) guard with isinstance + len for consistency
  with task_id handling.
- Import SCENARIOS from test_controller in mcp_tools.py so the
  comply_test_controller inputSchema enum is always derived from the
  canonical list and can't drift on the next scenario addition.

https://claude.ai/code/session_01KaGEJKsjnTEuLF6qnaRFqQ
@bokelley bokelley force-pushed the claude/issue-281-force-create-media-buy-arm-force-task-completion branch from 16b374d to fdf5588 Compare April 30, 2026 01:22
@bokelley bokelley marked this pull request as ready for review April 30, 2026 01:22
@bokelley bokelley merged commit 9818250 into main Apr 30, 2026
7 of 12 checks passed
@bokelley bokelley deleted the claude/issue-281-force-create-media-buy-arm-force-task-completion branch April 30, 2026 01:22
bokelley pushed a commit that referenced this pull request Apr 30, 2026
…m, force_task_completion, and seed_* scenarios

Fixes #312

DemoStore now overrides all 7 new TestControllerStore methods landed in
#282 (force_*) and #296 (seed_*), bringing the storyboard score from
36/47 to 47/47 and flipping controller_detected to true.

- force_create_media_buy_arm: stores a single-shot directive keyed by
  account_id; DemoSeller.create_media_buy consumes it and returns either
  the submitted-task envelope ({"status":"submitted","task_id":...}) or
  an input-required response ({"reason":"APPROVAL_REQUIRED"}).
- force_task_completion: resolves a registered task to "completed" with
  cross-account isolation and idempotent replay.
- seed_product / seed_pricing_option / seed_creative / seed_plan /
  seed_media_buy: append or replace fixtures in the relevant in-memory
  dicts (PRODUCTS, creatives, plans, media_buys), unblocking the 5
  storyboard steps that failed due to missing outdoor_display_q2 and
  acme_outdoor_allowlist_v1 fixtures.

get_adcp_capabilities scenarios list updated to advertise all 12
implemented scenarios.

https://claude.ai/code/session_01DJWM1a9nfjauGxSks9T1KW
bokelley added a commit that referenced this pull request Apr 30, 2026
Five fixups while taking PR #313 over from triage:

1. Lint blocker — duplicate "account" key in two dict literals
   (mcp_tools.py:853, test_controller.py:719). Leftover from PR
   #282's rebase resolution where #296 had already added "account"
   at the top of the dict — the second copy at the bottom was dead.
   Removing it unblocks ruff F601 on Python 3.13.

2. Re-apply valid_actions_for_status refactor on seller_agent.py
   that was lost in PR #310's squash-merge. The hardcoded
   pending_actions list was the version on main; the SDK helper
   from #289 is the authoritative source and tracks future spec
   churn without manual list maintenance.

3. Add sync_creatives -> pending_start transition on
   DemoSeller.sync_creatives. Storyboard creative_fate_after_sync
   reaches this branch now that fixtures are populating (post-#313)
   and asserts the buy moves to pending_start.

4. Trim compliance_testing.scenarios to schema-allowed names. AdCP
   3.0.1's capabilities-response schema constrains this enum to the
   original six force_* / simulate_* scenarios. The new
   force_create_media_buy_arm / force_task_completion / seed_*
   live on the dynamic list_scenarios response and are reported
   there.

5. End-to-end verified: 36/47 passing, matching pre-#313 baseline.
   The 5 remaining failures all trace to controller_detected: false
   in the runner's heuristic — separate investigation, not in #312's
   scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 30, 2026
…orce_task_completion, and seed_* scenarios (#313)

* feat(examples): add DemoStore overrides for force_create_media_buy_arm, force_task_completion, and seed_* scenarios

Fixes #312

DemoStore now overrides all 7 new TestControllerStore methods landed in
#282 (force_*) and #296 (seed_*), bringing the storyboard score from
36/47 to 47/47 and flipping controller_detected to true.

- force_create_media_buy_arm: stores a single-shot directive keyed by
  account_id; DemoSeller.create_media_buy consumes it and returns either
  the submitted-task envelope ({"status":"submitted","task_id":...}) or
  an input-required response ({"reason":"APPROVAL_REQUIRED"}).
- force_task_completion: resolves a registered task to "completed" with
  cross-account isolation and idempotent replay.
- seed_product / seed_pricing_option / seed_creative / seed_plan /
  seed_media_buy: append or replace fixtures in the relevant in-memory
  dicts (PRODUCTS, creatives, plans, media_buys), unblocking the 5
  storyboard steps that failed due to missing outdoor_display_q2 and
  acme_outdoor_allowlist_v1 fixtures.

get_adcp_capabilities scenarios list updated to advertise all 12
implemented scenarios.

https://claude.ai/code/session_01DJWM1a9nfjauGxSks9T1KW

* fix(examples,server): close 313 review issues + post-rebase regressions

Five fixups while taking PR #313 over from triage:

1. Lint blocker — duplicate "account" key in two dict literals
   (mcp_tools.py:853, test_controller.py:719). Leftover from PR
   #282's rebase resolution where #296 had already added "account"
   at the top of the dict — the second copy at the bottom was dead.
   Removing it unblocks ruff F601 on Python 3.13.

2. Re-apply valid_actions_for_status refactor on seller_agent.py
   that was lost in PR #310's squash-merge. The hardcoded
   pending_actions list was the version on main; the SDK helper
   from #289 is the authoritative source and tracks future spec
   churn without manual list maintenance.

3. Add sync_creatives -> pending_start transition on
   DemoSeller.sync_creatives. Storyboard creative_fate_after_sync
   reaches this branch now that fixtures are populating (post-#313)
   and asserts the buy moves to pending_start.

4. Trim compliance_testing.scenarios to schema-allowed names. AdCP
   3.0.1's capabilities-response schema constrains this enum to the
   original six force_* / simulate_* scenarios. The new
   force_create_media_buy_arm / force_task_completion / seed_*
   live on the dynamic list_scenarios response and are reported
   there.

5. End-to-end verified: 36/47 passing, matching pre-#313 baseline.
   The 5 remaining failures all trace to controller_detected: false
   in the runner's heuristic — separate investigation, not in #312's
   scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement force_create_media_buy_arm + force_task_completion controller scenarios for AdCP storyboard parity

2 participants