From 91029eb33c197d9447c1903c85ffd10f3d4741eb Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Tue, 18 Aug 2026 11:45:29 +0200 Subject: [PATCH] fix(media-buy): accept compact version envelope --- .changeset/compact-version-envelope.md | 6 + package.json | 2 +- .../media-buy/accept-proposal-request.json | 3 + .../media-buy/buy-products-request.json | 3 + .../media-buy/control-media-buy-request.json | 3 + tests/compact-version-envelope.test.cjs | 118 ++++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 .changeset/compact-version-envelope.md create mode 100644 tests/compact-version-envelope.test.cjs diff --git a/.changeset/compact-version-envelope.md b/.changeset/compact-version-envelope.md new file mode 100644 index 0000000000..ed00f87251 --- /dev/null +++ b/.changeset/compact-version-envelope.md @@ -0,0 +1,6 @@ +--- +"adcontextprotocol": patch +--- + +Accept the deprecated `adcp_major_version` compatibility field on every AdCP +3.2 compact media-buy lifecycle request. diff --git a/package.json b/package.json index 48a05f9c94..8b8a8a7b05 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "test:run-storyboards-schema-root": "node --test --test-force-exit --test-timeout=30000 tests/run-storyboards-schema-root-options.test.cjs", "test:storyboard-doc-parity": "node --test --test-force-exit --test-timeout=30000 tests/lint-universal-storyboard-doc-parity.test.cjs", "test:pagination-invariant": "node --test --test-force-exit --test-timeout=30000 tests/lint-pagination-invariant.test.cjs", - "test:version-envelope": "node --test --test-force-exit --test-timeout=30000 tests/lint-version-envelope.test.cjs", + "test:version-envelope": "node --test --test-force-exit --test-timeout=30000 tests/lint-version-envelope.test.cjs tests/compact-version-envelope.test.cjs", "test:test-dynamic-imports": "node --test --test-force-exit --test-timeout=30000 tests/lint-test-dynamic-imports.test.cjs", "test:sdk-shims": "node --test --test-force-exit --test-timeout=30000 tests/lint-sdk-shims.test.cjs tests/sdk-runtime-compat.test.cjs", "test:tenant-routing": "vitest run --config server/vitest.config.ts server/src/training-agent/tenants/tenant-smoke.test.ts", diff --git a/static/schemas/source/media-buy/accept-proposal-request.json b/static/schemas/source/media-buy/accept-proposal-request.json index c96a87c3ee..41a545d7d0 100644 --- a/static/schemas/source/media-buy/accept-proposal-request.json +++ b/static/schemas/source/media-buy/accept-proposal-request.json @@ -19,6 +19,9 @@ "adcp_version": { "$ref": "/schemas/core/version-envelope.json#/properties/adcp_version" }, + "adcp_major_version": { + "$ref": "/schemas/core/version-envelope.json#/properties/adcp_major_version" + }, "idempotency_key": { "type": "string", "minLength": 16, diff --git a/static/schemas/source/media-buy/buy-products-request.json b/static/schemas/source/media-buy/buy-products-request.json index 5a3821b196..bea34f69e8 100644 --- a/static/schemas/source/media-buy/buy-products-request.json +++ b/static/schemas/source/media-buy/buy-products-request.json @@ -14,6 +14,9 @@ "adcp_version": { "$ref": "/schemas/core/version-envelope.json#/properties/adcp_version" }, + "adcp_major_version": { + "$ref": "/schemas/core/version-envelope.json#/properties/adcp_major_version" + }, "idempotency_key": { "type": "string", "minLength": 16, diff --git a/static/schemas/source/media-buy/control-media-buy-request.json b/static/schemas/source/media-buy/control-media-buy-request.json index bd06bc3c2b..93d3938201 100644 --- a/static/schemas/source/media-buy/control-media-buy-request.json +++ b/static/schemas/source/media-buy/control-media-buy-request.json @@ -19,6 +19,9 @@ "adcp_version": { "$ref": "/schemas/core/version-envelope.json#/properties/adcp_version" }, + "adcp_major_version": { + "$ref": "/schemas/core/version-envelope.json#/properties/adcp_major_version" + }, "idempotency_key": { "type": "string", "minLength": 16, diff --git a/tests/compact-version-envelope.test.cjs b/tests/compact-version-envelope.test.cjs new file mode 100644 index 0000000000..35edf7f625 --- /dev/null +++ b/tests/compact-version-envelope.test.cjs @@ -0,0 +1,118 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); +const { ProtocolClient } = require('@adcp/sdk'); + +const SOURCE_DIR = path.resolve(__dirname, '..', 'static', 'schemas', 'source'); +const ADCP_VERSION = '3.2.0-beta.0'; +const EXPECTED_ENVELOPE = { + adcp_major_version: 3, + adcp_version: '3.2-beta.0', +}; + +function readSchema(uri) { + if (!uri.startsWith('/schemas/')) { + throw new Error(`Cannot load external schema: ${uri}`); + } + return JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, uri.slice('/schemas/'.length)), 'utf8')); +} + +async function compileRequestSchema(tool) { + const ajv = new Ajv({ + allErrors: true, + strict: false, + discriminator: true, + loadSchema: async (uri) => readSchema(uri), + }); + addFormats(ajv); + return ajv.compileAsync(readSchema(`/schemas/media-buy/${tool.replaceAll('_', '-')}-request.json`)); +} + +const compactRequests = { + list_products: {}, + request_proposals: { + idempotency_key: 'request-proposals-envelope-0001', + brand: { domain: 'nova-brands.example' }, + brief: 'Reach streaming audio listeners in Rome', + }, + refine_proposals: { + idempotency_key: 'refine-proposals-envelope-0001', + refinements: [{ proposal_id: 'proposal-1', action: 'finalize' }], + }, + decline_proposals: { + idempotency_key: 'decline-proposals-envelope-0001', + declines: [{ proposal_id: 'proposal-1', reason: 'inventory_fit' }], + }, + buy_products: { + idempotency_key: 'buy-products-envelope-0001', + account: { account_id: 'account-1' }, + brand: { domain: 'nova-brands.example' }, + feed_version: 'feed-version-1', + purchases: [{ + product_id: 'streaming-audio', + pricing_option_id: 'fixed-cpm', + budget: 50000, + }], + start_time: 'asap', + end_time: '2027-07-01T00:00:00Z', + }, + accept_proposal: { + idempotency_key: 'accept-proposal-envelope-0001', + account: { account_id: 'account-1' }, + proposal_id: 'proposal-1', + proposal_terms_digest: `sha256:${'A'.repeat(43)}`, + }, + control_media_buy: { + idempotency_key: 'control-media-buy-envelope-0001', + account: { account_id: 'account-1' }, + media_buy_id: 'media-buy-1', + revision: 1, + paused: true, + }, +}; + +test('SDK auto version envelope passes every strict compact lifecycle request schema', async () => { + const validators = new Map(await Promise.all( + Object.keys(compactRequests).map(async (tool) => [tool, await compileRequestSchema(tool)]), + )); + const outbound = new Map(); + const strictServer = { + transport: {}, + getServerCapabilities: () => ({}), + callTool: async ({ name, arguments: args }) => { + const validate = validators.get(name); + assert.ok(validate, `unexpected compact tool ${name}`); + assert.equal( + validate(args), + true, + `${name} rejected the SDK auto envelope: ${JSON.stringify(validate.errors)}`, + ); + outbound.set(name, args); + return { structuredContent: { status: 'completed' } }; + }, + }; + const agent = { + id: 'strict-compact-server', + name: 'Strict compact server', + agent_uri: 'adcp-in-process://strict-compact-server', + protocol: 'mcp', + _inProcessMcpClient: strictServer, + }; + + for (const [tool, request] of Object.entries(compactRequests)) { + await ProtocolClient.callTool(agent, tool, request, { adcpVersion: ADCP_VERSION }); + assert.deepEqual( + { + adcp_major_version: outbound.get(tool).adcp_major_version, + adcp_version: outbound.get(tool).adcp_version, + }, + EXPECTED_ENVELOPE, + `${tool} did not receive the SDK's default auto envelope`, + ); + } +});