fix(conformance): treat absent proposal support as unsupported#2248
Conversation
There was a problem hiding this comment.
LGTM as a 7.x backport. Spec text in the 3.1-beta schema (tools.generated.ts:8575: "When false or absent, the seller serves products directly without proposal abstraction; conformance runners skip proposal-lifecycle storyboards.") makes this a witness move, not a translator move — absence has a declared meaning for this specific field and the runner is honoring it.
Things I checked
- Changeset present,
patchis the right type — no public API change, runner-internal behavior only. isProposalLifecycleGatenarrows cleanly and is used at both call sites (runner.ts:289 in the predicate, runner.ts:1622 in the executeStoryboardPass branch).- Tests cover the two paths the runtime branch handles:
raw_capabilities: { media_buy: {} }with discovery tool present, and missingraw_capabilitieswith discovery tool absent. The directevaluateCapabilityPredicatetests cover absent/true/false. - No fabrication. No custom transport. No published-API change. Doc links unaffected.
Follow-ups (non-blocking — file as issues)
- Probe-failure gap.
runner.ts:1621-1634fires only whenget_adcp_capabilitiesis absent fromprofile.tools. When the tool IS advertised but the discovery call throws —client.ts:377-381setsprofile.capabilities_probe_errorand leavesraw_capabilitiesundefined — the new branch falls through and the proposal storyboard runs, producing exactly the misleading cascade this PR aims to prevent. Consider gating onrawCaps === undefinedregardless, or OR-inprofile.capabilities_probe_error !== undefined. Add a test for that path so it doesn't regress. - Wrong shape long-term.
isProposalLifecycleGatehardcoding the path stringmedia_buy.supports_proposalsis fine as a one-off but the same "absence = unsupported" semantics already apply tobuying_modes(defaults to['brief']when absent),reporting_delivery_methods(polling-only when absent), and others. The right primitive is a storyboard-side declaration on the gate itself (extending #1811'spresent:matcher, orrequires_capability: { ..., declared_true: true }) so the storyboard author opts in, not the runner. Track for main; the backport stays as-is. - Changeset prose. "skip proposal storyboards when supports_proposals is absent" understates the behavior change for adopters who were relying on the old run-and-fail mode to flush under-declaration. Worth a one-liner noting graded outcomes will shift for under-declared sellers.
Approving on the strength of the spec citation plus the test coverage matching the two runtime branches the PR actually adds.
There was a problem hiding this comment.
Clean backport of #2247. The narrow special-case is the right shape: an optional opt-in feature should fail-closed — absent media_buy.supports_proposals means "didn't opt in," so skip the proposal-lifecycle storyboard rather than run it and cascade misleading failures.
This does not break the general absence-semantics rule. The default "run when undeclared" behavior at runner.ts:286 is preserved for every other capability; the exception fires only when isProposalLifecycleGate narrows path === 'media_buy.supports_proposals' && equals === true. Sits after the present:/contains: early-returns, so no cross-contamination with those matchers.
code-reviewer: sound. Not a witness/translator violation — this is a test-applicability skip decision, not a fabricated capability value written back into the profile or wire response. Nothing synthetic crosses a seam.
Things I checked
- Dual-path logic. Path 1 (
runner.ts:1597, rawCaps defined):resolveCapabilityPathreturnsundefined→evaluateCapabilityPredicateskips via the new branch. Path 2 (runner.ts:1609else if, rawCaps undefined + noget_adcp_capabilities): skips. Both terminate inbuildCapabilityUnsupportedResult. Test 1 exercises Path 1, Test 2 exercises Path 2. null-on-wire and primitive-intermediate cases. Both still skip — via the existingactual !== undefined && actual !== predicate.equalsbranch — so the outcome is consistent regardless of which branch catches them.profile === undefinedshort-circuits Path 2 safely (storyboard runs — reasonable default when there's no profile at all).- Changeset present,
patchbump. Correct: internal-only refinement of conformance-runner skip behavior, no exported-symbol or response-shape change. Changeset-vs-wire-impact audit passes. - Test assertions match the actual skip-detail strings (
media_buy.supports_proposals/did not declare/not satisfied);overall_passed,skipped_count,skip_reason, and theDETAILED_SKIP_TO_CANONICALmapping are all exercised.
Follow-ups (non-blocking — file as issues)
- Scenario C asymmetry on probe failure. The Path 2 guard
!profile.tools.includes('get_adcp_capabilities')(runner.ts:1609) only catches "agent doesn't advertise the tool." If the agent advertisesget_adcp_capabilitiesbut the probe threw or returned nothing,rawCapsis undefined AND the tool is inprofile.tools— neither branch fires, and the proposal storyboard runs. That's a defensible choice ("support unknown because probe failed" ≠ "support absent"), but it's undocumented. A one-line comment on theelse ifwould stop a future reader filing it as a bug.
Minor nits (non-blocking)
- Stale doc.
src/lib/testing/storyboard/types.ts:132-133still says "Whenraw_capabilitiesis not available... the gate is a no-op and the storyboard runs." Now false for the proposal gate. Mirror therunner.ts:286exception note here. - Hardcoded
=== truein the detail string. The new message atevaluateCapabilityPredicatetemplates=== trueliterally rather than interpolatingpredicate.equalslike its sibling branch. Correct becauseisProposalLifecycleGatenarrowsequalstotrue— not fabricated, just stylistically inconsistent with the${JSON.stringify(predicate.equals)}neighbor.
Approving. Follow-ups noted above.
Summary
media_buy.supports_proposalsas unsupported for proposal-lifecyclerequires_capabilitygates.Fixes #2245 for 7.x / 7.11.x.
Validation
npm ciin the backport worktreenpm run build:libNODE_ENV=test node --test-timeout=60000 --test-force-exit --test test/lib/storyboard-capability-gate.test.jsnpx prettier --check src/lib/testing/storyboard/runner.ts test/lib/storyboard-capability-gate.test.jsgit diff --checknpm run typecheck+ build hook