diff --git a/.changeset/version-translation-vectors.md b/.changeset/version-translation-vectors.md new file mode 100644 index 0000000000..1a07631c28 --- /dev/null +++ b/.changeset/version-translation-vectors.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": patch +--- + +Compliance: add `version-translation` test vectors for cross-version translation between consecutive minor releases (proposed DR-0032 / DR-0033, aligned with DR-0036), one file per pair: `3.2-3.1.json` and `3.3-3.2.json` (settled surfaces, provisional until 3.3 GA). They cover client request down-conversion and response up-conversion, server response down-conversion, refusal of mutations whose lost field would change the commitment, exact field moves, envelope fields kept, release negotiation, and one-to-one reduction of the 3.2 account key. diff --git a/docs/reference/test-vectors/index.mdx b/docs/reference/test-vectors/index.mdx index 1eaf3ea51e..91e3113cdc 100644 --- a/docs/reference/test-vectors/index.mdx +++ b/docs/reference/test-vectors/index.mdx @@ -50,6 +50,7 @@ SDKs SHOULD fetch versioned paths where available and record the version under t | [`brand-response-signing`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/brand-response-signing) | Brand-authorization cross-check for `verify_brand_claim` and `verify_brand_claims` envelopes: advisory binding produces verifier-local untrusted results without hard-rejecting the response or rewriting signer-asserted `verification_status`. Mandatory cryptographic brand authorization is deferred to 4.0 | `static/compliance/source/test-vectors/brand-response-signing/` | `/compliance/latest/test-vectors/brand-response-signing/` | | [`oauth-setup`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/oauth-setup) | OAuth metadata-graph conformance: deterministic network fixtures for `oauth_metadata_graph` checks, redirect validation, graph-budget enforcement, and fail-closed behavior for missing fixture responses. No live network access permitted | `static/compliance/source/test-vectors/oauth-setup/` | `/compliance/latest/test-vectors/oauth-setup/` | | [`trusted-match-context-merge`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/trusted-match-context-merge) | Context Match targeting key-value merge: router attribution from publisher-controlled provider registration, bucket-key derivation, provider `signals.targeting_kvs` pass-through, shared-key multi-provider contribution, and dropped unmapped/case-mismatched tuples | `static/compliance/source/test-vectors/trusted-match-context-merge/` | `/compliance/latest/test-vectors/trusted-match-context-merge/` | +| [`version-translation`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/version-translation) | Cross-version translation between consecutive minor releases, one file per pair (`3.2-3.1.json`, `3.3-3.2.json`, the latter provisional until 3.3 GA): request down-conversion and response up-conversion on the client, response down-conversion on the server, reads that drop versus mutations that refuse, exact field moves, envelope fields kept, release negotiation, and one-to-one reduction of the 3.2 account key (proposed, DR-0032 / DR-0033 / DR-0036) | `static/compliance/source/test-vectors/version-translation/` | `/compliance/latest/test-vectors/version-translation/` | | [`products-only-brief-compatibility`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/products-only-brief-compatibility) | Products-only brief flow across AdCP 2.5, 3.0, 3.1: compact projection without invented proposals, fail-closed legacy-create continuation with named fences, seller-fenced `listed_purchase` feed/pricing propagation, and 3.2 reverse-compatibility facades | `static/compliance/source/test-vectors/products-only-brief-compatibility/` | `/compliance/latest/test-vectors/products-only-brief-compatibility/` | | [`price-adjustments`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/price-adjustments) | Deterministic resolution of selection-dependent `price_adjustments`: one row per dimension (first match), sequential running total in declared order (index cards as fee and discount rates), quote binding to account and expiry, accepted terms pricing a permitted change without granting one, half-away-from-zero rounding at option price precision (JPY, sub-cent `cpv`), declared-breakdown composition, cross-encoding and cross-scope selector identity, below-zero rejection, buyer-pricing mismatch, new-ID-on-change, and schema negatives (next 3.3 beta) | `static/compliance/source/test-vectors/price-adjustments/` | `/compliance/latest/test-vectors/price-adjustments/` | | [`error-recovery`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/error-recovery) | Version-gated 3.2 recovery emission and compatible 3.1 decoding: known/unknown codes with and without `recovery`, buyer-reason consistency, bounded fallback retries, and `retry_after` scheduling including legacy fractions | `static/compliance/source/test-vectors/error-recovery/` | `/compliance/latest/test-vectors/error-recovery/` | diff --git a/static/compliance/source/test-vectors/version-translation/3.2-3.1.json b/static/compliance/source/test-vectors/version-translation/3.2-3.1.json new file mode 100644 index 0000000000..9378c9bbba --- /dev/null +++ b/static/compliance/source/test-vectors/version-translation/3.2-3.1.json @@ -0,0 +1,1167 @@ +{ + "description": "Cross-version translation between AdCP 3.2 and 3.1 (DR-0032, DR-0033), chained to 3.0 where noted: client request down-conversion and response up-conversion, server response down-conversion, release negotiation, and one-to-one reduction of the account key 3.2 changed on schemas 3.1 published closed.", + "reference_implementation": "@adcp/sdk (adcontextprotocol/adcp-client#3372, #3376)", + "version_pair": [ + "3.2", + "3.1" + ], + "cases": [ + { + "id": "create_media_buy-bid-amount-moves", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "A media-buy bid_amount default is inherited as package bid_price; a package bid_amount moves to its own bid_price. Labels and rotation hints drop.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "creative_assignments": [ + { + "creative_id": "cr_1", + "rotation_mode": "even" + } + ] + }, + { + "product_id": "prod_2", + "pricing_option_id": "po_2", + "budget": 500, + "bidding": { + "bid_amount": 9 + } + } + ], + "name": "Q1 launch", + "bidding": { + "bid_amount": 4.5 + } + }, + "expect": { + "outcome": "converted", + "output": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "creative_assignments": [ + { + "creative_id": "cr_1" + } + ], + "bid_price": 4.5 + }, + { + "product_id": "prod_2", + "pricing_option_id": "po_2", + "budget": 500, + "bid_price": 9 + } + ] + }, + "dropped": [ + "packages[0].creative_assignments[0].rotation_mode", + "name" + ], + "moved": [ + { + "from": "bidding", + "to": "packages[0].bid_price" + }, + { + "from": "packages[1].bidding", + "to": "packages[1].bid_price" + } + ] + } + }, + { + "id": "create_media_buy-default-overridden", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "A media-buy default that every package overrides is redundant and is removed, not refused.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "bid_price": 3 + } + ], + "bidding": { + "bid_amount": 4.5 + } + }, + "expect": { + "outcome": "converted", + "output": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "bid_price": 3 + } + ] + }, + "dropped": [ + "bidding" + ], + "moved": [] + } + }, + { + "id": "create_media_buy-max-bid-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "max_bid has no safe 3.1 form without the pricing option flag, so the mutation is refused.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "bidding": { + "max_bid": 9 + } + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "packages[0].bidding" + ] + } + }, + { + "id": "create_media_buy-daily-cap-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "A spend cap the older release cannot express is refused on a mutation.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000 + } + ], + "daily_budget_cap": 100 + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "daily_budget_cap" + ] + } + }, + { + "id": "create_media_buy-exclusion-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "A targeting exclusion the older release cannot express is refused on a mutation.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "targeting_overlay": { + "geo_countries": [ + "US" + ], + "device_platform_exclude": [ + "ios" + ] + } + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "packages[0].targeting_overlay.device_platform_exclude" + ] + } + }, + { + "id": "update_media_buy-any-lost-field-refused", + "side": "client", + "direction": "request_down", + "tool": "update_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "On an update, any lost field is a requested change that would not happen.", + "input": { + "idempotency_key": "vt-update-media-buy-0001", + "media_buy_id": "mb_1", + "name": "Renamed" + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "name" + ] + } + }, + { + "id": "create_media_buy-account-identity-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "account_identity": "refuse", + "note": "Under accountIdentityDowngrade 'refuse', an account key field the older release cannot express is refused.", + "input": { + "idempotency_key": "vt-create-media-buy-0001", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example", + "operator_unit": { + "id": "desk-ca" + } + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000 + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "account_identity", + "fields": [ + "account.operator_unit" + ] + } + }, + { + "id": "sync_accounts-account-identity-reduced", + "side": "client", + "direction": "request_down", + "tool": "sync_accounts", + "from_version": "3.2", + "to_version": "3.1", + "account_identity": "reduce", + "note": "Under 'reduce', the older account key is sent and the reduction reported.", + "input": { + "idempotency_key": "vt-sync-accounts-0001", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD", + "billing": "operator" + } + ] + }, + "expect": { + "outcome": "converted", + "output": { + "idempotency_key": "vt-sync-accounts-0001", + "accounts": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example", + "billing": "operator" + } + ] + }, + "dropped": [], + "moved": [], + "account_identity_reduced": [ + "accounts[0].brand.countries", + "accounts[0].currency" + ] + } + }, + { + "id": "sync_accounts-account-move-refused", + "side": "client", + "direction": "request_down", + "tool": "sync_accounts", + "from_version": "3.2", + "to_version": "3.1", + "note": "A 3.2 rekey or operator handoff (operator_identity, revision) has no 3.1 form.", + "input": { + "idempotency_key": "vt-sync-accounts-0002", + "accounts": [ + { + "account": { + "account_id": "acct_1" + }, + "operator_identity": { + "operator": "pinnacle-media.example", + "operator_unit": { + "id": "desk-ca" + } + }, + "revision": 3 + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "accounts[0].operator_identity", + "accounts[0].revision" + ] + } + }, + { + "id": "get_products-read-drops", + "side": "client", + "direction": "request_down", + "tool": "get_products", + "from_version": "3.2", + "to_version": "3.1", + "note": "On a read, fields the older release cannot express drop and are reported. A top-level brand is not an account key.", + "input": { + "brief": "running shoes", + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "targeting_overlay": { + "geo_countries": [ + "CA" + ] + } + }, + "expect": { + "outcome": "converted", + "output": { + "brief": "running shoes", + "brand": { + "domain": "acme.example" + } + }, + "dropped": [ + "brand.countries", + "targeting_overlay" + ], + "moved": [] + } + }, + { + "id": "get_media_buy_delivery-read-drops", + "side": "client", + "direction": "request_down", + "tool": "get_media_buy_delivery", + "from_version": "3.2", + "to_version": "3.1", + "note": "Newer reporting requests drop on a read.", + "input": { + "media_buy_ids": [ + "mb_1" + ], + "requested_metrics": [ + "impressions" + ], + "reporting_dimensions": { + "creative": {} + } + }, + "expect": { + "outcome": "converted", + "output": { + "media_buy_ids": [ + "mb_1" + ], + "reporting_dimensions": {} + }, + "dropped": [ + "requested_metrics", + "reporting_dimensions.creative" + ], + "moved": [] + } + }, + { + "id": "sync_creatives-assign-operations-move", + "side": "client", + "direction": "request_down", + "tool": "sync_creatives", + "from_version": "3.2", + "to_version": "3.1", + "note": "assign-only assignment_operations become 3.1 assignments, validated strictly to keep 3.2 atomicity.", + "input": { + "idempotency_key": "vt-sync-creatives-0001", + "assignment_operations": [ + { + "operation": "assign", + "creative_id": "cr_1", + "package_id": "pkg_1", + "weight": 50 + }, + { + "operation": "assign", + "creative_id": "cr_2", + "package_id": "pkg_2" + } + ] + }, + "expect": { + "outcome": "converted", + "output": { + "idempotency_key": "vt-sync-creatives-0001", + "assignments": [ + { + "creative_id": "cr_1", + "package_id": "pkg_1", + "weight": 50 + }, + { + "creative_id": "cr_2", + "package_id": "pkg_2" + } + ], + "validation_mode": "strict" + }, + "dropped": [], + "moved": [ + { + "from": "assignment_operations[0]", + "to": "assignments[0]" + }, + { + "from": "assignment_operations[1]", + "to": "assignments[1]" + } + ] + } + }, + { + "id": "sync_creatives-unassign-refused", + "side": "client", + "direction": "request_down", + "tool": "sync_creatives", + "from_version": "3.2", + "to_version": "3.1", + "note": "unassign and replace have no 3.1 form.", + "input": { + "idempotency_key": "vt-sync-creatives-0002", + "assignment_operations": [ + { + "operation": "unassign", + "creative_id": "cr_1", + "package_id": "pkg_1" + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "assignment_operations" + ] + } + }, + { + "id": "report_plan_outcome-cumulative-spend-refused", + "side": "client", + "direction": "request_down", + "tool": "report_plan_outcome", + "from_version": "3.2", + "to_version": "3.1", + "note": "3.2 cumulative_spend is lifetime spend; 3.1 spend is period spend. They cannot be equated, so the call is refused.", + "input": { + "idempotency_key": "vt-report-plan-outcome-0001", + "plan_id": "plan_1", + "outcome": "delivery", + "governance_context": "eyJhbGciOi...", + "delivery": { + "observation_id": "obs_1", + "source": "buyer_measurement", + "observed_at": "2027-01-02T00:00:00Z", + "reporting_period": { + "start": "2027-01-01T00:00:00Z", + "end": "2027-01-02T00:00:00Z" + }, + "cumulative_spend": 120, + "currency": "USD" + } + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "delivery.observation_id", + "delivery.source", + "delivery.observed_at", + "delivery.cumulative_spend", + "delivery.currency" + ] + } + }, + { + "id": "get_products-chain-to-3.0", + "side": "client", + "direction": "request_down", + "tool": "get_products", + "from_version": "3.2", + "to_version": "3.0", + "note": "Larger gaps chain one minor at a time: 3.2 to 3.1 drops targeting_overlay, then 3.1 to 3.0 drops brand_kit_override.", + "input": { + "brief": "running shoes", + "brand": { + "domain": "acme.example", + "brand_kit_override": { + "colors": { + "accent": "#000000" + } + } + }, + "targeting_overlay": { + "geo_countries": [ + "US" + ] + } + }, + "expect": { + "outcome": "converted", + "output": { + "brief": "running shoes", + "brand": { + "domain": "acme.example" + } + }, + "dropped": [ + "targeting_overlay", + "brand.brand_kit_override" + ], + "moved": [] + } + }, + { + "id": "create_media_buy-status-alias", + "side": "client", + "direction": "response_up", + "tool": "create_media_buy", + "from_version": "3.1", + "to_version": "3.2", + "note": "The 3.1 media-buy status alias moves to media_buy_status.", + "input": { + "adcp_version": "3.1", + "status": "active", + "media_buy_id": "mb_1", + "packages": [] + }, + "expect": { + "outcome": "converted", + "output": { + "adcp_version": "3.1", + "status": "completed", + "media_buy_id": "mb_1", + "packages": [], + "media_buy_status": "active" + }, + "moved": [ + { + "from": "status", + "to": "media_buy_status" + } + ] + } + }, + { + "id": "get_media_buy_delivery-currency-per-buy", + "side": "client", + "direction": "response_up", + "tool": "get_media_buy_delivery", + "from_version": "3.1", + "to_version": "3.2", + "note": "The single 3.1 root currency is copied onto each media buy.", + "input": { + "currency": "USD", + "reporting_period": { + "start": "2027-01-01T00:00:00Z", + "end": "2027-01-02T00:00:00Z" + }, + "media_buy_deliveries": [ + { + "media_buy_id": "mb_1", + "status": "active", + "totals": { + "impressions": 1000, + "spend": 12.5 + }, + "by_package": [] + } + ] + }, + "expect": { + "outcome": "converted", + "output": { + "currency": "USD", + "reporting_period": { + "start": "2027-01-01T00:00:00Z", + "end": "2027-01-02T00:00:00Z" + }, + "media_buy_deliveries": [ + { + "media_buy_id": "mb_1", + "status": "active", + "totals": { + "impressions": 1000, + "spend": 12.5 + }, + "by_package": [], + "currency": "USD" + } + ] + }, + "moved": [ + { + "from": "currency", + "to": "media_buy_deliveries[0].currency" + } + ] + } + }, + { + "id": "get_media_buy_delivery-root-currency", + "side": "server", + "direction": "response_down", + "tool": "get_media_buy_delivery", + "from_version": "3.2", + "to_version": "3.1", + "note": "A shared per-buy currency becomes the root currency 3.1 requires; 3.2-only fields drop.", + "input": { + "reporting_period": { + "start": "2027-01-01T00:00:00Z", + "end": "2027-01-02T00:00:00Z" + }, + "media_buy_deliveries": [ + { + "media_buy_id": "mb_1", + "status": "active", + "currency": "USD", + "pacing_index": 1.02, + "totals": { + "impressions": 1000, + "spend": 12.5 + }, + "by_package": [] + } + ] + }, + "expect": { + "outcome": "converted", + "output": { + "reporting_period": { + "start": "2027-01-01T00:00:00Z", + "end": "2027-01-02T00:00:00Z" + }, + "media_buy_deliveries": [ + { + "media_buy_id": "mb_1", + "status": "active", + "totals": { + "impressions": 1000, + "spend": 12.5 + }, + "by_package": [] + } + ], + "currency": "USD" + }, + "dropped": [ + "media_buy_deliveries[0].currency", + "media_buy_deliveries[0].pacing_index" + ], + "moved": [ + { + "from": "media_buy_deliveries[].currency", + "to": "currency" + } + ] + } + }, + { + "id": "create_media_buy-response-drops", + "side": "server", + "direction": "response_down", + "tool": "create_media_buy", + "from_version": "3.2", + "to_version": "3.1", + "note": "Response fields the older release cannot express drop.", + "input": { + "status": "completed", + "media_buy_id": "mb_1", + "media_buy_status": "active", + "name": "Q1 launch", + "packages": [] + }, + "expect": { + "outcome": "converted", + "output": { + "status": "completed", + "media_buy_id": "mb_1", + "media_buy_status": "active", + "packages": [] + }, + "dropped": [ + "name" + ], + "moved": [] + } + }, + { + "id": "list_account_changes-not-defined", + "side": "client", + "direction": "request_down", + "tool": "list_account_changes", + "from_version": "3.2", + "to_version": "3.1", + "note": "A task the negotiated release does not define is refused before sending, unless the seller lists the tool.", + "input": { + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + }, + "expect": { + "outcome": "refused", + "reason": "task_not_defined" + } + } + ], + "negotiation": [ + { + "client_version": "3.2", + "seller_supported_versions": [ + "3.0", + "3.1" + ], + "note": "Highest release on or below the client line.", + "expect": { + "outcome": "negotiated", + "version": "3.1" + } + }, + { + "client_version": "3.2", + "seller_supported_versions": [ + "3.1", + "3.2" + ], + "note": "Same line: the client release is spoken.", + "expect": { + "outcome": "negotiated", + "version": "3.2" + } + }, + { + "client_version": "3.2-beta.6", + "seller_supported_versions": [ + "3.1", + "3.2" + ], + "note": "A prerelease client does not match a GA seller of its line.", + "expect": { + "outcome": "negotiated", + "version": "3.1" + } + }, + { + "client_version": "3.2", + "seller_supported_versions": [ + "3.0", + "3.1-beta.1" + ], + "note": "Older-line prereleases are never selected.", + "expect": { + "outcome": "negotiated", + "version": "3.0" + } + }, + { + "client_version": "3.1", + "seller_supported_versions": [ + "3.2" + ], + "note": "No release on or below the client line.", + "expect": { + "outcome": "no_common_version" + } + } + ], + "account_sequences": [ + { + "id": "reduced-then-second-key", + "note": "A second buyer key that reduces onto an already-claimed seller account is refused.", + "seller_version": "3.1", + "steps": [ + { + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD" + } + ], + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ] + } + }, + { + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "US" + ] + }, + "operator": "pinnacle-media.example", + "currency": "USD" + } + ], + "expect": { + "outcome": "refused", + "reason": "account_identity_collision" + } + } + ] + }, + { + "id": "plain-then-reduced", + "note": "A key sent unreduced claims itself, so a later key that reduces onto it is refused.", + "seller_version": "3.1", + "steps": [ + { + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ], + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ] + } + }, + { + "tool": "create_media_buy", + "account": { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD" + }, + "expect": { + "outcome": "refused", + "reason": "account_identity_collision" + } + }, + { + "tool": "create_media_buy", + "account": { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + }, + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ] + } + } + ] + }, + { + "id": "collision-within-one-request", + "note": "Claims are all-or-nothing per request: a refused request claims nothing.", + "seller_version": "3.1", + "steps": [ + { + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD" + }, + { + "brand": { + "domain": "acme.example", + "countries": [ + "US" + ] + }, + "operator": "pinnacle-media.example", + "currency": "USD" + } + ], + "expect": { + "outcome": "refused", + "reason": "account_identity_collision" + } + }, + { + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "US" + ] + }, + "operator": "pinnacle-media.example", + "currency": "USD" + } + ], + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ] + } + } + ] + }, + { + "id": "seller-upgrade-keeps-reduced-key", + "note": "After the seller upgrades, a reduced account is still addressed by its reduced key until the buyer migrates it.", + "steps": [ + { + "seller_version": "3.1", + "tool": "sync_accounts", + "accounts": [ + { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD" + } + ], + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ] + } + }, + { + "seller_version": "3.2", + "tool": "create_media_buy", + "account": { + "brand": { + "domain": "acme.example", + "countries": [ + "CA" + ] + }, + "operator": "pinnacle-media.example", + "currency": "CAD" + }, + "expect": { + "outcome": "sent", + "account_keys": [ + { + "brand": { + "domain": "acme.example" + }, + "operator": "pinnacle-media.example" + } + ], + "migration_pending": { + "fields": [ + "currency", + "brand.countries" + ], + "in_place": false + } + } + } + ] + } + ], + "status": "proposed (DR-0032, DR-0033)" +} diff --git a/static/compliance/source/test-vectors/version-translation/3.3-3.2.json b/static/compliance/source/test-vectors/version-translation/3.3-3.2.json new file mode 100644 index 0000000000..c5c0f10f5b --- /dev/null +++ b/static/compliance/source/test-vectors/version-translation/3.3-3.2.json @@ -0,0 +1,335 @@ +{ + "description": "Cross-version translation between AdCP 3.3 and 3.2 (DR-0032, DR-0033, DR-0036): settled 3.3 surfaces only. Provisional until 3.3 GA: 3.3 closed-schema dispositions (#8501) land before GA and are added then.", + "status": "proposed; provisional until 3.3 GA", + "reference_implementation": "@adcp/sdk (adcontextprotocol/adcp-client), validated against 3.3.0-beta.3 and 3.2.8", + "version_pair": [ + "3.3", + "3.2" + ], + "cases": [ + { + "id": "get_products-read-drops-3.3-flags", + "side": "client", + "direction": "request_down", + "tool": "get_products", + "from_version": "3.3", + "to_version": "3.2", + "note": "On a read, 3.3 request flags drop for a 3.2 seller and are reported; the call proceeds.", + "input": { + "buying_mode": "brief", + "brief": "running shoes", + "include_collections": true, + "include_execution_readiness": true + }, + "expect": { + "outcome": "converted", + "output": { + "buying_mode": "brief", + "brief": "running shoes" + }, + "dropped": [ + "include_collections", + "include_execution_readiness" + ], + "moved": [] + } + }, + { + "id": "create_media_buy-creative-gate-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.3", + "to_version": "3.2", + "note": "Gated: a 3.2 seller would ignore the creative-approval start gate and could start delivery early, so the mutation is refused.", + "input": { + "idempotency_key": "vt33-create-media-buy-0001", + "account": { + "account_id": "acct_acme" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "start_requires_all_creatives_approved": true + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "packages[0].start_requires_all_creatives_approved" + ] + } + }, + { + "id": "create_media_buy-assignment-trackers-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.3", + "to_version": "3.2", + "note": "Gated: a 3.2 seller would ignore per-assignment trackers, so measurement would silently be missing. Refused.", + "input": { + "idempotency_key": "vt33-create-media-buy-0001", + "account": { + "account_id": "acct_acme" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "creative_assignments": [ + { + "creative_id": "cr_1", + "trackers": [ + { + "event": "impression", + "url": "https://tracker.example/i" + } + ] + } + ] + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "packages[0].creative_assignments[0].trackers" + ] + } + }, + { + "id": "create_media_buy-collection-lists-refused", + "side": "client", + "direction": "request_down", + "tool": "create_media_buy", + "from_version": "3.3", + "to_version": "3.2", + "note": "Gated: a 3.2 seller would ignore the collection-list intersection and serve outside it. Refused.", + "input": { + "idempotency_key": "vt33-create-media-buy-0001", + "account": { + "account_id": "acct_acme" + }, + "brand": { + "domain": "acme.example" + }, + "start_time": "asap", + "end_time": "2027-01-31T23:59:59Z", + "packages": [ + { + "product_id": "prod_1", + "pricing_option_id": "po_1", + "budget": 1000, + "targeting_overlay": { + "collection_lists": [ + { + "agent_url": "https://lists.example/mcp", + "list_id": "cl_1" + }, + { + "agent_url": "https://lists.example/mcp", + "list_id": "cl_2" + } + ] + } + } + ] + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "packages[0].targeting_overlay.collection_lists" + ] + } + }, + { + "id": "request_proposals-product-universe-refused", + "side": "client", + "direction": "request_down", + "tool": "request_proposals", + "from_version": "3.3", + "to_version": "3.2", + "note": "Gated, on a reopened proposal-flow root: a 3.2 seller would ignore product_ids_exclusive and compose beyond the named products. Refused.", + "input": { + "idempotency_key": "vt33-request-proposals-0001", + "account": { + "account_id": "acct_acme" + }, + "brand": { + "domain": "acme.example" + }, + "brief": "Q1 launch across the named products only", + "criteria": { + "product_ids": [ + "prod_1", + "prod_2" + ], + "product_ids_exclusive": true + } + }, + "expect": { + "outcome": "refused", + "reason": "field_not_expressible", + "fields": [ + "criteria.product_ids_exclusive" + ] + } + }, + { + "id": "list_products-response-hold-policy-dropped", + "side": "server", + "direction": "response_down", + "tool": "list_products", + "from_version": "3.3", + "to_version": "3.2", + "note": "Server: a 3.3 field on the reopened proposal flow (hold_policy on a canonical product) goes only to 3.3 peers; for a 3.2 buyer it is removed and reported.", + "input": { + "status": "completed", + "outcome": "listed", + "feed_version": "feed-2027-01", + "cache_scope": "public", + "products": [ + { + "product_id": "prod_1", + "name": "Premium video", + "hold_policy": { + "available": false + } + } + ] + }, + "expect": { + "outcome": "converted", + "output": { + "status": "completed", + "outcome": "listed", + "feed_version": "feed-2027-01", + "cache_scope": "public", + "products": [ + { + "product_id": "prod_1", + "name": "Premium video" + } + ] + }, + "dropped": [ + "products[0].hold_policy" + ], + "moved": [] + } + }, + { + "id": "list_products-response-echo-kept", + "side": "client", + "direction": "response_up", + "tool": "list_products", + "from_version": "3.2", + "to_version": "3.3", + "note": "Client: up-conversion never removes. The adcp_version echo a 3.2.8 seller returns (DR-0036 correction b) is kept.", + "input": { + "status": "completed", + "outcome": "listed", + "feed_version": "feed-2027-01", + "cache_scope": "public", + "products": [ + { + "product_id": "prod_1", + "name": "Premium video" + } + ], + "adcp_version": "3.2" + }, + "expect": { + "outcome": "converted", + "output": { + "status": "completed", + "outcome": "listed", + "feed_version": "feed-2027-01", + "cache_scope": "public", + "products": [ + { + "product_id": "prod_1", + "name": "Premium video" + } + ], + "adcp_version": "3.2" + }, + "moved": [] + } + }, + { + "id": "list_proposals-not-defined", + "side": "client", + "direction": "request_down", + "tool": "list_proposals", + "from_version": "3.3", + "to_version": "3.2", + "note": "A task 3.2 does not define is refused before sending, unless the seller lists the tool.", + "input": { + "account": { + "account_id": "acct_acme" + } + }, + "expect": { + "outcome": "refused", + "reason": "task_not_defined" + } + } + ], + "negotiation": [ + { + "client_version": "3.3", + "seller_supported_versions": [ + "3.1", + "3.2" + ], + "note": "A 3.3 client speaks 3.2 to a 3.2 seller.", + "expect": { + "outcome": "negotiated", + "version": "3.2" + } + }, + { + "client_version": "3.3-beta.3", + "seller_supported_versions": [ + "3.2", + "3.3" + ], + "note": "A prerelease client does not match the GA seller of its line; it falls back to 3.2.", + "expect": { + "outcome": "negotiated", + "version": "3.2" + } + }, + { + "client_version": "3.3", + "seller_supported_versions": [ + "3.2", + "3.3-beta.3" + ], + "note": "A GA client never selects a seller prerelease of its own line; it speaks 3.2.", + "expect": { + "outcome": "negotiated", + "version": "3.2" + } + } + ] +} diff --git a/static/compliance/source/test-vectors/version-translation/README.md b/static/compliance/source/test-vectors/version-translation/README.md new file mode 100644 index 0000000000..5f27fe1177 --- /dev/null +++ b/static/compliance/source/test-vectors/version-translation/README.md @@ -0,0 +1,43 @@ +# Version translation vectors + +These vectors pin how SDKs translate between consecutive minor releases of a major (DR-0032, DR-0033, DR-0036). Application code speaks the newest release its SDK supports, and the SDK converts for older counterparts. Every SDK runs the same cases, so a buyer and a seller on different SDKs and releases behave the same way. + +## Files + +One file per consecutive minor pair: + +- **`3.2-3.1.json`**: 3.2 ↔ 3.1, with one case chained to 3.0. Includes the account key 3.2 changed on schemas 3.1 published closed, which DR-0036 records as a case that stays until 4.0. +- **`3.3-3.2.json`**: 3.3 ↔ 3.2, settled surfaces only. Provisional until 3.3 GA: the 3.3 closed-schema dispositions from #8501 land before GA, and their cases are added then. The 3.2 side is validated against 3.2.8, which carries the DR-0036 corrections. + +Each later pair (3.4 ↔ 3.3, then 4.1 ↔ 4.0) adds its own file under DR-0033. + +## Format + +Each file has these sections: + +- **`cases`**: one payload conversion each. + - **`side`** is `client` or `server`. + - **`direction`** is one of: + - `request_down`: a client converts a request for an older seller; + - `response_up`: a client converts an older seller's response; + - `response_down`: a server converts a handler's response for an older buyer. + - **`expect.outcome`** is `converted` or `refused`: + - `converted` gives the exact `output`, the JSON paths that MUST be reported as `dropped`, and the `moved` fields; + - `refused` gives a `reason` (`field_not_expressible`, `account_identity`, `task_not_defined`) and, where applicable, the refused `fields`. A refused request MUST NOT be sent. + - **`account_identity`**, where present, is the account-key policy the case runs under (`refuse` or `reduce`). +- **`negotiation`**: the release a client speaks for a seller's `supported_versions`. Same line: the client's release. Otherwise: the highest older GA line. A prerelease matches only its exact prerelease, on either side, and older-line prereleases are never selected. +- **`account_sequences`** (3.2 ↔ 3.1 only): ordered calls under one-to-one key reduction. + - **One-to-one:** a reduced account key may serve one buyer key only, and a key sent unreduced claims itself. A step that would put a second buyer key on the same seller account is refused, in either order. + - **Atomic:** claims are all-or-nothing per request. + - **After a seller upgrade:** the reduced key stays in use until the buyer migrates the account. `migration_pending.in_place` is true only when the reduction removed nothing but `operator_unit`. + +## Rules the vectors encode + +- **Newer fields go only to peers that negotiated them.** Under DR-0036 an older peer accepts and ignores a field on an open schema, so the hazard is a silent ignore, not a rejection. +- **Reads drop; mutations refuse.** On a read, a field the older release doesn't define is removed and reported. On a mutation, only labels and hints may be removed. A field whose loss would change spend, targeting, caps, bidding, creative activation, measurement or account identity is refused before sending. +- **Moves are exact.** For example, `bidding.bid_amount` becomes package `bid_price`. A media-buy default is inherited by packages without their own bid, and a default every package overrides is removed. Assign-only `assignment_operations` become `assignments` with `validation_mode: "strict"`. `max_bid`, `cost_per` and `roas` have no safe 3.1 form. +- **Envelope fields are never removed.** The protocol and version envelope apply to every task payload, even where an older task schema didn't compose them (DR-0036 correction b). +- **Up-conversion never removes.** +- **Chains** run one minor at a time. + +The expected values were generated by the reference implementation, `@adcp/sdk` (adcontextprotocol/adcp-client), and reviewed by hand. Every 3.3 ↔ 3.2 input validates against 3.3.0-beta.3, and every converted output against 3.2.8. They are `proposed` until DR-0032 and DR-0033 are ratified.