diff --git a/.changeset/gate-creative-library-storyboard.md b/.changeset/gate-creative-library-storyboard.md new file mode 100644 index 0000000000..aa03e4142c --- /dev/null +++ b/.changeset/gate-creative-library-storyboard.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": patch +--- + +Gate the `creative_fate_after_cancellation` compliance storyboard on sellers advertising `creative.has_creative_library: true`. Sellers without declared creative-library support now exclude this scenario from runnable coverage, which can correct compliance badge and completeness-score denominators. diff --git a/static/compliance/source/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml b/static/compliance/source/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml index 8aa713e72a..963114b654 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml @@ -1,9 +1,14 @@ id: media_buy_seller/creative_fate_after_cancellation -version: "1.0.0" +version: "1.0.1" title: "Creative lifecycle is decoupled from media buy lifecycle" category: media_buy_seller summary: "Validates that canceling a media buy releases package-creative assignments but leaves the underlying creatives in the library with their review state intact, and that buyers can reuse released creatives on a new buy." track: media_buy + +requires_capability: + path: creative.has_creative_library + equals: true + required_tools: - get_products - create_media_buy diff --git a/tests/sdk-runner-capability-gates.test.cjs b/tests/sdk-runner-capability-gates.test.cjs index 5ea04c0122..fb3ab5f984 100644 --- a/tests/sdk-runner-capability-gates.test.cjs +++ b/tests/sdk-runner-capability-gates.test.cjs @@ -94,6 +94,69 @@ test('inventory-list storyboards skip sellers that declare property-list support } }); +test('creative fate storyboard requires advertised creative library support', async () => { + const storyboardPath = path.join( + __dirname, + '..', + 'static', + 'compliance', + 'source', + 'protocols', + 'media-buy', + 'scenarios', + 'creative_fate_after_cancellation.yaml' + ); + const storyboard = YAML.parse(fs.readFileSync(storyboardPath, 'utf8')); + const tools = ['get_adcp_capabilities', ...storyboard.required_tools]; + + assert.deepEqual(storyboard.requires_capability, { + path: 'creative.has_creative_library', + equals: true, + }); + + const unsupportedProfiles = [ + { + rawCapabilities: { creative: { has_creative_library: false } }, + errorPattern: /creative\.has_creative_library/, + }, + { + rawCapabilities: {}, + errorPattern: /agent did not declare support/, + }, + ]; + + for (const { rawCapabilities, errorPattern } of unsupportedProfiles) { + const result = await runStoryboard('https://agent.example/mcp', storyboard, { + _profile: { tools, raw_capabilities: rawCapabilities }, + agentTools: tools, + }); + + assert.equal(result.overall_passed, true); + assert.equal(result.failed_count, 0); + assert.equal(result.skipped_count, 1); + assert.equal(result.phases.length, 1); + assert.equal(result.phases[0].phase_id, 'capability_unsupported'); + assert.equal(result.phases[0].steps[0].skip_reason, 'capability_unsupported'); + assert.match(result.phases[0].steps[0].error, errorPattern); + } + + const supported = await runStoryboard( + 'https://agent.example/mcp', + { ...storyboard, prerequisites: undefined, phases: [] }, + { + _profile: { + tools, + raw_capabilities: { creative: { has_creative_library: true } }, + }, + agentTools: tools, + } + ); + + assert.equal(supported.overall_passed, true); + assert.equal(supported.phases[0].phase_id, 'no_phases'); + assert.equal(supported.phases[0].steps[0].skip_reason, 'no_phases'); +}); + test('inventory-list no-match requires canonical rejection and fails accepted buys', async () => { const storyboardPath = path.join( __dirname,