Skip to content

feat(testing): brand-rights + SI seeded bridges (#1755 phase 4)#1773

Merged
bokelley merged 2 commits into
mainfrom
feat/seeded-brand-rights-si-bridges
May 16, 2026
Merged

feat(testing): brand-rights + SI seeded bridges (#1755 phase 4)#1773
bokelley merged 2 commits into
mainfrom
feat/seeded-brand-rights-si-bridges

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Phase 4 of the TestControllerBridge seeded-callback umbrella (#1755). Extends TestControllerBridge<TAccount> with three opt-in callbacks for brand-rights and sponsored-intelligence read tools:

Callback Tool Shape Dedup key
getSeededBrandIdentity get_brand_identity singleton replace brand_id
getSeededRights get_rights append-merge rights_id
getSeededSiOffering si_get_offering singleton replace offering.offering_id

SI investigation outcome

The umbrella issue flagged SI as a "design wildcard" because the SI flow has session-keyed state (si_initiate_session issues sessions; si_send_message advances them). Schema audit shows si_get_offering is the OUT-LIER inside that flow — it is a stateless catalog lookup:

  • Request schema (SIGetOfferingRequest, src/lib/types/tools.generated.ts:12571): top-level offering_id: string is the only addressing field. No session_id, no offering_token.
  • Response schema (SIGetOfferingResponse, line 12600): the response PRODUCES an offering_token for a FUTURE session but does not CONSUME one. The lookup itself is request-shaped, not session-shaped.

Conclusion: the standard singleton-replace pattern fits cleanly. Bridge is shipped.

The session-stateful SI tools (si_initiate_session, si_send_message, si_terminate_session) remain intentionally out of scope — a seeded fixture cannot honestly serve session state accumulated across prior conversational turns. If a future storyboard needs to seed in-session messages, that will need a different seam (likely session-store-keyed, not bridge-keyed).

Schema citations

  • get_brand_identityGetBrandIdentityRequest (core.generated.ts:5939, brand_id: string required), GetBrandIdentitySuccess (line 5996, top-level brand_id: string). Singleton response.
  • get_rightsGetRightsRequest (line 6328, NL query: string), GetRightsSuccess.rights[] (line 6386, each entry has rights_id: string). Array response (discovery / search). No pagination / query_summary per AdCP 3.0.11.
  • si_get_offeringSIGetOfferingRequest (tools.generated.ts:12571, offering_id: string required), SIGetOfferingResponse (line 12600, offering?: { offering_id?: string, ... }). Singleton response.

Tools skipped + why

  • acquire_rights / update_rights — mutating; seeded read paths only per phase 4 scope. The original umbrella explicitly lists these as out of scope.
  • si_initiate_session / si_send_message / si_terminate_session — mutating + session-stateful. A seeded fixture would lie about session state. Documented in code + changeset.
  • creative_approval — listed under BrandRightsHandlers in the SDK but is webhook-only (not an MCP/A2A tool) per AdCP 3.0; not a discovery / read tool.

Established precedents followed

  • Seeded wins on collision (both list path and singleton path)
  • Triply-gated dispatch: controller present + sandbox marker on request + resolved account is sandbox: true when present
  • Singleton replace preserves both handler's context AND ext (PR feat(testing): governance/property-lists/content-standards/collection-lists seeded bridges (#1755 phase 2) #1761 lesson)
  • Validation: warn-and-drop, never throws
  • Duplicate-id detection on singleton replace fixtures (first occurrence wins, mirrors filterValidSeededAccountFinancials)
  • Type imports from generated wire types; core.generated for brand-rights types (per src/lib/types/index.ts:268), tools.generated for SI types

Test count delta

+21 new tests, all passing.

  • 6 get_brand_identity (replace match / no match / context+ext preserve / sandbox-gate / validation-drop+duplicate)
  • 6 get_rights (append / seeded-only / handler-only / collision / sandbox-gate / validation-drop)
  • 6 si_get_offering (replace match / no match / context+ext preserve / sandbox-gate / validation-drop+duplicate)
  • 3 bridgeFromSessionStore selector wiring (one per new callback)
  • 1 selector-omission check

Total: 8716 pass / 0 fail across full npm test.

Doc + selectors added

  • BridgeFromSessionStoreOptions gains selectSeededBrandIdentity, selectSeededRights, selectSeededSiOffering
  • docs/guides/VALIDATE-YOUR-AGENT.md "Platform-proxy sellers" table extended with the three new selectors mapped to their specialism phases (brand-rights identity / rights discovery; sponsored-intelligence offering lookup)
  • Validation-drop summary extended with the new dedup keys

Test plan

  • npm run typecheck — clean on changed files
  • npm run format:check — clean
  • npm test (full root suite) — 8716 pass / 0 fail
  • npm run build:lib — succeeds

Related

🤖 Generated with Claude Code

bokelley added a commit that referenced this pull request May 16, 2026
Captures protocol-review nit on PR #1773: when a stateful SI session
bridge ships (phase 5+ of #1755), the seeded offering_token here must
match the seeded session's offering_token, or si_initiate_session will
reject as stale-token. Today si_initiate_session is out of scope so no
coupling exists; the note is a tripwire for future readers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Doc-only nit addressed

Added a JSDoc note on replaceSiOfferingIfSeeded capturing the future-coupling concern raised in protocol review: when phase 5 adds a stateful SI session bridge, the seeded offering_token here must match the seeded session's offering_token. Today, si_initiate_session is explicitly out of scope so no coupling exists; the comment is a tripwire for future readers.

bokelley and others added 2 commits May 16, 2026 06:10
Extends `TestControllerBridge<TAccount>` with three opt-in callbacks so
platform-proxy sellers can seed brand-rights and sponsored-intelligence
fixtures into conformance storyboards without driving real upstream
calls:

  - getSeededBrandIdentity  (brand_id)            → get_brand_identity   (singleton replace)
  - getSeededRights         (rights_id)           → get_rights           (append-merge)
  - getSeededSiOffering     (offering.offering_id)→ si_get_offering      (singleton replace)

Singleton replace for `get_brand_identity` and `si_get_offering` mirrors
the precedent set by `replaceContentStandardsIfSeeded` — seeded fixture
is authoritative on the response body; framework-managed `context` /
`ext` round-trip from the handler. Append-merge for `get_rights` mirrors
`getSeededProducts` — `get_rights` is a discovery / search tool with an
NL `query`, so the response carries an array; no pagination /
query_summary blocks per AdCP 3.0.11.

`si_get_offering` is a stateless catalog lookup despite the broader SI
flow being session-keyed: the response PRODUCES an `offering_token` for
a future session but does not CONSUME one, so the singleton-replace
pattern fits cleanly. The session-stateful SI tools
(`si_initiate_session`, `si_send_message`, `si_terminate_session`) are
intentionally NOT bridged — they consume session state that a static
fixture can't honestly serve. The mutating brand-rights tools
(`acquire_rights`, `update_rights`) are NOT bridged either — seeded read
paths only, per phase 4 scope.

Same triply-gated sandbox contract (controller present + sandbox marker
on request + resolved account is `sandbox: true` when `resolveAccount`
produced one), seeded-wins collision precedent, warn-and-drop validation
contract (never throws). `BridgeFromSessionStoreOptions` gains matching
`selectSeededBrandIdentity` / `selectSeededRights` /
`selectSeededSiOffering` selectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures protocol-review nit on PR #1773: when a stateful SI session
bridge ships (phase 5+ of #1755), the seeded offering_token here must
match the seeded session's offering_token, or si_initiate_session will
reject as stale-token. Today si_initiate_session is out of scope so no
coupling exists; the note is a tripwire for future readers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the feat/seeded-brand-rights-si-bridges branch from c60adfd to 46b1fce Compare May 16, 2026 10:19
// body; handler's `context` / `ext` round-trip. The response is
// a union (success | error) — the dispatcher already gated on
// `!isErrorResponse`, so we narrow defensively when reading.
else if (toolName === 'get_brand_identity' && testControllerBridge.getSeededBrandIdentity) {
// get_rights — append-merge keyed by `rights_id`. Discovery /
// search tool (NL `query`); the response carries `rights[]`.
// Drops to a no-op on the error arm of the response union.
else if (toolName === 'get_rights' && testControllerBridge.getSeededRights) {
// Stateless catalog lookup; the response's `offering_token` is
// produced for a future session but the lookup itself does not
// consume one. Handler's `context` / `ext` round-trip.
else if (toolName === 'si_get_offering' && testControllerBridge.getSeededSiOffering) {
@bokelley bokelley merged commit 4348d51 into main May 16, 2026
10 checks passed
@bokelley bokelley deleted the feat/seeded-brand-rights-si-bridges branch May 16, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant