Skip to content

fix(storyboard): proposal-mode create_media_buy request shape (closes #1600)#1603

Merged
bokelley merged 1 commit intomainfrom
claude/issue-1600-fix-proposal-mode-create-media-buy
May 8, 2026
Merged

fix(storyboard): proposal-mode create_media_buy request shape (closes #1600)#1603
bokelley merged 1 commit intomainfrom
claude/issue-1600-fix-proposal-mode-create-media-buy

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 8, 2026

Summary

Closes #1600. Supersedes #1599's allowlist workaround.

After AdCP 3.0.7 (#1595) landed adcp#4088, the proposal_finalize storyboard runs end-to-end through accept_proposal for the first time. The runner's create_media_buy enricher in src/lib/testing/storyboard/request-builder.ts was unaware of the proposal-mode request shape — it always returned { account, brand, start_time, end_time, packages } even when the storyboard's sample_request carried proposal_id: "$context.proposal_id".

Two failures, layered

  1. Schema rejection — AdCP 3.0.7's create-media-buy-request.json declares dependencies.proposal_id: ["total_budget"] and disallows packages alongside proposal_id. Synthesising packages with proposal_id elided made the request fail validation against the buyer-side strict gate. Error: "must have property total_budget when property proposal_id is present".
  2. Account resolutioncreate_media_buy is in FIXTURE_AWARE_ENRICHERS, so the enricher's output is used verbatim and the fixture's account does not flow through the generic merge. The non-proposal path always replaces account with resolveAccount(options) (default brand test.example); proposal-mode storyboards author a non-default brand (acmeoutdoor.example) that the adapter resolved end-to-end through brief/refine/finalize, so the override produced ACCOUNT_NOT_FOUND at the accept step.

Fix

The enricher detects proposal-mode (either step.sample_request.proposal_id resolving via $context.* or context.proposal_id set directly) and returns:

  • The fixture spread (so total_budget and other proposal-mode-required fields flow through).
  • Harness-normalised start_time / end_time (the existing future-dating logic still applies for replay determinism).
  • proposal_id from context.
  • account and brand prefer the fixture when supplied; otherwise the same context.account ?? resolveAccount(options) / resolveBrand(options) fallback as the non-proposal path.
  • packages explicitly stripped (schema disallows it alongside proposal_id).

Test plan

  • npm run format:check clean
  • npx tsc --noEmit clean
  • node --test test/examples/hello-seller-adapter-proposal-mode.test.js — 3/3 pass (storyboard end-to-end, sole-stateful exemption invariants, façade gate including new order endpoints)
  • Adjacent storyboard suites — storyboard-runner-contract, storyboard-no-phases, storyboard-idempotency-invariant — 58/58 pass
  • EXPECTED_FAILURES cleared (both get_products_refine and create_media_buy now pass on their own merits)
  • expectedRoutes extended with POST /v1/orders and POST /v1/orders/{id}/lineitems — the façade gate now asserts the full proposal lifecycle reaches the upstream's order endpoints

🤖 Generated with Claude Code

…1600)

After AdCP 3.0.7 (#1595) landed adcp#4088, the proposal_finalize
storyboard runs end-to-end through accept_proposal for the first
time. The runner's create_media_buy enricher in request-builder.ts
was unaware of the proposal-mode request shape — it always returned
{ account, brand, start_time, end_time, packages } even when the
storyboard's sample_request carried `proposal_id: "$context.proposal_id"`.

Two failures fell out:

1. Schema rejection. AdCP 3.0.7's create-media-buy-request.json
   declares `dependencies.proposal_id: ["total_budget"]` and
   disallows `packages` alongside `proposal_id`. Synthesising
   packages with the proposal_id elided made the request fail
   validation against the buyer-side strict gate.

2. Account resolution. create_media_buy is in
   FIXTURE_AWARE_ENRICHERS, so the enricher's output is used
   verbatim and the fixture's account does not flow through the
   generic merge. The non-proposal path always replaces account
   with resolveAccount(options) (default brand `test.example`);
   proposal-mode storyboards author a non-default brand
   (`acmeoutdoor.example`) that the adapter resolved end-to-end
   through brief/refine/finalize, so the override produced
   ACCOUNT_NOT_FOUND at the accept step.

The enricher now detects proposal-mode (either
step.sample_request.proposal_id resolving via $context.* or
context.proposal_id set directly) and returns the fixture spread
(with total_budget and other proposal-mode-required fields
preserved) plus the harness-normalised start_time / end_time and
proposal_id. account and brand prefer the fixture when supplied so
non-default brands survive to the wire; otherwise the same
context.account ?? resolveAccount(options) fallback applies.

hello_seller_adapter_proposal_mode regression coverage updated:
EXPECTED_FAILURES cleared (both get_products_refine — fixed by
adcp#4088 — and create_media_buy — fixed here). expectedRoutes
extended with POST /v1/orders and POST /v1/orders/{id}/lineitems
so the façade gate now asserts the full proposal lifecycle reaches
the upstream's order endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the claude/issue-1600-fix-proposal-mode-create-media-buy branch from d11c14a to ea1e2e2 Compare May 8, 2026 15:17
@bokelley bokelley changed the title fix(storyboard): forward proposal_id from context into create_media_buy request fix(storyboard): proposal-mode create_media_buy request shape (closes #1600) May 8, 2026
@bokelley bokelley marked this pull request as ready for review May 8, 2026 15:17
@bokelley bokelley merged commit 20fce14 into main May 8, 2026
11 checks passed
bokelley added a commit that referenced this pull request May 8, 2026
… mutating-tool requests (#1604) (#1607)

Fixture-aware enrichers in `request-builder.ts` rebuilt their request body
from scratch and only copied an enumerated set of fields from
`step.sample_request` (start_time, end_time, packages for create_media_buy).
Anything else the storyboard authored at the top level — `total_budget`,
`buyer_ref`, `currency`, `reporting_webhook`, scenario-specific extensions
— was silently dropped before the request hit the wire. The non-proposal
`create_media_buy` path was the immediate trigger; PR #1603's proposal-mode
branch already spread the fixture but the structural fix wasn't applied to
the rest of the enricher.

The fix spreads `sample_request` first (after `$context` injection), then
layers the runner's substitutions (account, brand, normalised dates,
packages with discovery-derived identifiers) on top. Envelope fields
(`context`, `ext`, `push_notification_config`, `idempotency_key`) are
deliberately omitted from the local spread and re-applied by the outer
`enrichRequest` with `runnerVars` so mustache substitutions like
`{{runner.webhook_url:<step_id>}}` expand correctly. The same
`omitEnvelopeFields` discipline is now applied uniformly across
`create_media_buy`, `update_media_buy`, `get_media_buys`,
`get_media_buy_delivery`, and `comply_test_controller`.

Audit of FIXTURE_AWARE_ENRICHERS (the only path where the bug manifests
because they bypass the outer fixture overlay):

- `create_media_buy` non-proposal — fixed (build-from-scratch dropped fields)
- `create_media_buy` proposal — already spread, hardened with envelope
  omission so push_notification_config tokens expand correctly
- `update_media_buy` — already spread; hardened with envelope omission
- `get_media_buys` — already spread; hardened with envelope omission
- `get_media_buy_delivery` — already spread; hardened with envelope omission
- `comply_test_controller` — already spread; hardened with envelope omission

Non-FIXTURE_AWARE enrichers don't manifest the bug because the outer
`enrichRequest` already does `{ ...enriched, ...fixture }` so the fixture
overlays the enricher's hardcoded keys.

Regression test pinning the field-pass-through is added to
`test/lib/request-builder.test.js` (verified to fail when the fix is
reverted). The proposal-mode adapter gate's EXPECTED_FAILURES allowlist
remains empty.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(adapter): hello_seller_adapter_proposal_mode create_media_buy fails schema validation against AdCP 3.0.7

1 participant