Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/fix-get-rights-request-builder.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 15 additions & 1 deletion src/lib/testing/storyboard/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,25 @@ const REQUEST_BUILDERS: Record<string, RequestBuilder> = {
},

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,
};
},

Expand Down
23 changes: 23 additions & 0 deletions test/lib/request-builder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ 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', () => {
Expand Down
Loading