Skip to content

Implement force_create_media_buy_arm + force_task_completion controller scenarios for AdCP storyboard parity #281

@bokelley

Description

@bokelley

Context

The AdCP spec recently added two compliance-controller scenarios for testing the async create_media_buy lifecycle deterministically:

  • force_create_media_buy_arm (adcp#3104) — registers a single-shot directive that drives the next create_media_buy call into a specific arm (submitted, input-required) with a buyer-supplied task_id. Sellers MUST honor the directive verbatim on the next call from the same authenticated sandbox account.
  • force_task_completion (adcp#3138) — resolves a previously-submitted task to completed with a buyer-supplied result payload (validated against async-response-data.json). Cross-account replays return NOT_FOUND; identical-params replays are idempotent; diverging-params replays against a terminal task return INVALID_TRANSITION.

The training-agent in the adcp repo implements both (#3115, #3194). For storyboard-runner conformance against adcp-client-python to grade passing rather than not_applicable, the Python reference seller needs parity implementations.

Scope

Add the two scenarios to whatever this repo's compliance-controller surface looks like (mirror the structure of the existing force_*_status and simulate_* scenarios):

  1. force_create_media_buy_arm with params { arm: 'submitted' | 'input-required', task_id?: string, message?: string }. Validate task_id is required when arm = submitted, max 128 chars; message max 2000 chars. Single-shot per (account, principal): consumed by the next create_media_buy, then cleared. A second registration before consumption overwrites. Returns ForcedDirectiveSuccess shape.
  2. force_task_completion with params { task_id: string, result: object }. Validate task_id ≤128 chars, result is a non-empty object (256 KB soft cap). Records (task_id, result, ownerKey). Cross-account replays → NOT_FOUND. Identical-params replays → idempotent. Diverging-params replays against terminal → INVALID_TRANSITION. Returns StateTransitionSuccess with previous_state: 'submitted' / current_state: 'completed'.
  3. Advertise both in list_scenarios so storyboard runners detect support.

Reference implementations (mechanical port):

  • adcp/server/src/training-agent/comply-test-controller.ts (force_create_media_buy_arm + force_task_completion handlers, lines ~470-700)
  • adcp/server/tests/unit/training-agent-force-create-media-buy-arm.test.ts (test patterns)
  • adcp/server/tests/unit/training-agent-force-task-completion.test.ts (test patterns)

Acceptance

  • Sellers built on adcp-client-python running the AdCP storyboard suite hit the create_media_buy_async.yaml scenario and grade passing on the submitted-arm phase.
  • list_scenarios advertises both new entries.
  • Test coverage at parity with the training-agent's nine-test pattern: registration with valid params, INVALID_PARAMS branches, replay idempotency, diverging-replay INVALID_TRANSITION, cross-account isolation, list_scenarios advertisement.

Caveats

  • Buyer-side polling integration (the tasks/get round-trip) is deferred until two upstream gaps in @adcp/client (Node) are resolved: InMemoryTaskStore parity for caller-supplied IDs and AdCP-shaped tasks/get response handler. Tracked in adcp-client#994. This PR's scope is the controller-side primitive only.
  • Whether this repo's task store has caller-supplied-id support is its own design question. The training-agent uses a process-global Map (no SDK task-store integration) for the same reasons.

Related

  • adcp#3104 — force_create_media_buy_arm spec
  • adcp#3138 — force_task_completion spec
  • adcp#3115, adcp#3194 — Node training-agent implementations to mirror
  • adcp-client#994 — umbrella issue for the upstream gaps

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions