Conversation
…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>
d11c14a to
ea1e2e2
Compare
4 tasks
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>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1600. Supersedes #1599's allowlist workaround.
After AdCP 3.0.7 (#1595) landed adcp#4088, the
proposal_finalizestoryboard runs end-to-end throughaccept_proposalfor the first time. The runner'screate_media_buyenricher insrc/lib/testing/storyboard/request-builder.tswas unaware of the proposal-mode request shape — it always returned{ account, brand, start_time, end_time, packages }even when the storyboard'ssample_requestcarriedproposal_id: "$context.proposal_id".Two failures, layered
create-media-buy-request.jsondeclaresdependencies.proposal_id: ["total_budget"]and disallowspackagesalongsideproposal_id. Synthesisingpackageswithproposal_idelided made the request fail validation against the buyer-side strict gate. Error: "must have property total_budget when property proposal_id is present".create_media_buyis inFIXTURE_AWARE_ENRICHERS, so the enricher's output is used verbatim and the fixture'saccountdoes not flow through the generic merge. The non-proposal path always replacesaccountwithresolveAccount(options)(default brandtest.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 producedACCOUNT_NOT_FOUNDat the accept step.Fix
The enricher detects proposal-mode (either
step.sample_request.proposal_idresolving via$context.*orcontext.proposal_idset directly) and returns:total_budgetand other proposal-mode-required fields flow through).start_time/end_time(the existing future-dating logic still applies for replay determinism).proposal_idfrom context.accountandbrandprefer the fixture when supplied; otherwise the samecontext.account ?? resolveAccount(options)/resolveBrand(options)fallback as the non-proposal path.packagesexplicitly stripped (schema disallows it alongsideproposal_id).Test plan
npm run format:checkcleannpx tsc --noEmitcleannode --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)storyboard-runner-contract,storyboard-no-phases,storyboard-idempotency-invariant— 58/58 passEXPECTED_FAILUREScleared (bothget_products_refineandcreate_media_buynow pass on their own merits)expectedRoutesextended withPOST /v1/ordersandPOST /v1/orders/{id}/lineitems— the façade gate now asserts the full proposal lifecycle reaches the upstream's order endpoints🤖 Generated with Claude Code