From 745b7132c6da2057ef5021bb00dc2c53acc394e1 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 22 Apr 2026 03:10:05 -0400 Subject: [PATCH 1/2] fix(testing): honor sample_request in get_rights builder (closes adcp#2846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The get_rights request builder hardcoded query/uses and injected brand_id from the caller's brand.domain. Storyboards declaring scenario-specific query text, uses, or a buyer_brand hit the wire with the generic fallback, and rights-holder rosters rejected the caller-domain brand_id as unknown — so rights[0] was undefined, \$context.rights_id didn't resolve, and downstream acquire_rights steps failed with rights_not_found instead of the error the storyboard was actually asserting (e.g., GOVERNANCE_DENIED in the brand_rights/governance_denied scenario). Delegate to step.sample_request when present, mirroring peer builders (sync_plans, check_governance, list_creative_formats, create_content_standards, etc.). The generic fallback still runs when no sample_request is authored. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/fix-get-rights-request-builder.md | 24 +++++++++++++++++++ src/lib/testing/storyboard/request-builder.ts | 16 ++++++++++++- test/lib/request-builder.test.js | 24 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-get-rights-request-builder.md diff --git a/.changeset/fix-get-rights-request-builder.md b/.changeset/fix-get-rights-request-builder.md new file mode 100644 index 000000000..6d7049518 --- /dev/null +++ b/.changeset/fix-get-rights-request-builder.md @@ -0,0 +1,24 @@ +--- +'@adcp/client': patch +--- + +Storyboard runner: honor `step.sample_request` in `get_rights` +request builder. + +Prior behavior hardcoded `query: 'available rights for advertising'` +and `uses: ['ai_generated_image']`, and injected `brand_id` from the +caller's `brand.domain`. Storyboards declaring scenario-specific +query text, uses, or a `buyer_brand` hit the wire with the generic +fallback instead, and rights-holder rosters rejected the +caller-domain `brand_id` as unknown — so `rights[0]` was undefined, +`$context.rights_id` didn't resolve, and downstream `acquire_rights` +steps failed with `rights_not_found` instead of the error the +storyboard was actually asserting (e.g., `GOVERNANCE_DENIED` in +`brand_rights/governance_denied`). + +Mirrors the pattern used by peer builders (`sync_plans`, +`check_governance`, `list_creative_formats`, +`create_content_standards`, etc.). The generic fallback still runs +when no `sample_request` is authored. + +Closes adcp#2846. diff --git a/src/lib/testing/storyboard/request-builder.ts b/src/lib/testing/storyboard/request-builder.ts index c497d68f0..3dab2b394 100644 --- a/src/lib/testing/storyboard/request-builder.ts +++ b/src/lib/testing/storyboard/request-builder.ts @@ -76,11 +76,25 @@ const REQUEST_BUILDERS: Record = { }, get_rights(step, context, options) { + // Honor hand-authored sample_request so storyboards can specify + // scenario-specific query text, uses, countries, or buyer_brand. + // Peer builders (sync_plans, check_governance, list_creative_formats, + // create_content_standards, etc.) follow the same pattern. + // + // Without this, any get_rights step hits the wire with the generic + // fallback and a brand_id derived from the caller's domain — which + // rights-holder rosters reject as unknown, so rights[0] is undefined, + // $context.rights_id doesn't resolve, and downstream acquire_rights + // steps fail with rights_not_found instead of the error the + // storyboard is actually asserting (e.g., GOVERNANCE_DENIED). + if (step.sample_request) { + return injectContext({ ...step.sample_request }, context); + } const brand = resolveBrand(options); return { query: 'available rights for advertising', uses: ['ai_generated_image'], - brand_id: context.brand_id ?? (step.sample_request?.brand_id as string) ?? brand.brand_id ?? brand.domain, + brand_id: context.brand_id ?? brand.brand_id ?? brand.domain, }; }, diff --git a/test/lib/request-builder.test.js b/test/lib/request-builder.test.js index 008a5cb7d..b445a92f9 100644 --- a/test/lib/request-builder.test.js +++ b/test/lib/request-builder.test.js @@ -75,6 +75,30 @@ describe('Request Builder', () => { assert.ok(Array.isArray(result.uses), 'should have uses array'); assert.strictEqual(result.brand_id, 'acmeoutdoor.example'); }); + + test('honors step.sample_request when present', () => { + // Regression: the builder previously ignored sample_request for + // everything except brand_id, so a storyboard declaring specific + // query text / uses / countries hit the wire with the generic + // fallback. That silently broke scenarios like + // brand_rights/governance_denied where the buyer query matters + // for the rights-holder roster to return a non-empty list. + const fixture = { + buyer: { domain: 'pinnacle-agency.example' }, + query: 'licensed commercial rights for a regional outdoor retail campaign', + uses: ['commercial', 'endorsement'], + }; + const result = buildRequest( + step('get_rights', { sample_request: fixture }), + {}, + DEFAULT_OPTIONS, + ); + assert.strictEqual(result.query, fixture.query); + assert.deepStrictEqual(result.uses, fixture.uses); + assert.deepStrictEqual(result.buyer, fixture.buyer); + assert.strictEqual(result.brand_id, undefined, + 'brand_id from caller domain must not leak when sample_request omits it'); + }); }); describe('sync_catalogs', () => { From 482b0d475f03061d8ec8fdedee48e1654055912d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 22 Apr 2026 03:13:17 -0400 Subject: [PATCH 2/2] style: prettier fix on request-builder.test.js Co-Authored-By: Claude Opus 4.7 (1M context) --- test/lib/request-builder.test.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/lib/request-builder.test.js b/test/lib/request-builder.test.js index b445a92f9..db6efe886 100644 --- a/test/lib/request-builder.test.js +++ b/test/lib/request-builder.test.js @@ -88,16 +88,15 @@ describe('Request Builder', () => { query: 'licensed commercial rights for a regional outdoor retail campaign', uses: ['commercial', 'endorsement'], }; - const result = buildRequest( - step('get_rights', { sample_request: fixture }), - {}, - DEFAULT_OPTIONS, - ); + const result = buildRequest(step('get_rights', { sample_request: fixture }), {}, DEFAULT_OPTIONS); assert.strictEqual(result.query, fixture.query); assert.deepStrictEqual(result.uses, fixture.uses); assert.deepStrictEqual(result.buyer, fixture.buyer); - assert.strictEqual(result.brand_id, undefined, - 'brand_id from caller domain must not leak when sample_request omits it'); + assert.strictEqual( + result.brand_id, + undefined, + 'brand_id from caller domain must not leak when sample_request omits it' + ); }); });