@adcp/sdk 17.0.0-beta.6 → 17.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/testing/storyboard/default-invariants.js +4 -0
- package/dist/lib/testing/storyboard/default-invariants.mjs +4 -0
- package/dist/lib/testing/storyboard/loader.js +31 -0
- package/dist/lib/testing/storyboard/loader.mjs +31 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.d.mts +13 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.d.ts +13 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.js +175 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.mjs +151 -0
- package/dist/lib/testing/storyboard/request-builder.js +6 -3
- package/dist/lib/testing/storyboard/request-builder.mjs +6 -3
- package/dist/lib/testing/storyboard/required-metric-filters.d.mts +9 -0
- package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
- package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
- package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
- package/dist/lib/testing/storyboard/runner.js +244 -48
- package/dist/lib/testing/storyboard/runner.mjs +246 -49
- package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
- package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
- package/dist/lib/testing/storyboard/seeding.js +91 -18
- package/dist/lib/testing/storyboard/seeding.mjs +90 -18
- package/dist/lib/testing/storyboard/types.d.mts +30 -1
- package/dist/lib/testing/storyboard/types.d.ts +30 -1
- package/dist/lib/testing/storyboard/types.js +1 -0
- package/dist/lib/testing/storyboard/types.mjs +1 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
- package/docs/llms.txt +2 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -132,7 +132,8 @@ import { REQUEST_SIGNING_PROBE_TASK } from "./request-signing/synthesize.mjs";
|
|
|
132
132
|
import { createWebhookReceiver } from "./webhook-receiver.mjs";
|
|
133
133
|
import { WEBHOOK_ASSERTION_TASKS, armWebhookAssertions, executeWebhookAssertionStep } from "./webhook-assertions.mjs";
|
|
134
134
|
import {
|
|
135
|
-
runControllerSeeding
|
|
135
|
+
runControllerSeeding,
|
|
136
|
+
collectWholesaleProductPages
|
|
136
137
|
} from "./seeding.mjs";
|
|
137
138
|
import { callControllerRaw } from "../test-controller.mjs";
|
|
138
139
|
import { applyFixtureBindingsToRequest } from "./fixture-resolution.mjs";
|
|
@@ -1187,6 +1188,7 @@ function buildPhaseCapabilitySkippedSteps(storyboard, phase, detail, context) {
|
|
|
1187
1188
|
}
|
|
1188
1189
|
const REQUIREMENT_TO_SKIP_REASON = {
|
|
1189
1190
|
controller: "missing_test_controller",
|
|
1191
|
+
collect_product_pages_runner: "requirement_unmet",
|
|
1190
1192
|
seeded_state: "requirement_unmet",
|
|
1191
1193
|
real_wire: "requirement_unmet",
|
|
1192
1194
|
webhook_receiver: "requirement_unmet",
|
|
@@ -1320,6 +1322,8 @@ async function checkRequires(requires, storyboard, options, profile, precomputed
|
|
|
1320
1322
|
continue;
|
|
1321
1323
|
}
|
|
1322
1324
|
switch (requirement) {
|
|
1325
|
+
case "collect_product_pages_runner":
|
|
1326
|
+
break;
|
|
1323
1327
|
case "controller": {
|
|
1324
1328
|
if (!options.agentTools) continue;
|
|
1325
1329
|
if (!options.agentTools.includes("comply_test_controller")) {
|
|
@@ -3553,6 +3557,11 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
3553
3557
|
}
|
|
3554
3558
|
assignment ??= dispatch.nextFor(step);
|
|
3555
3559
|
const stepExecutionState = buildExecutionState(assignment.agentUrl, assignment.profile);
|
|
3560
|
+
stepExecutionState.onCollectedProductPage = async (page, pageIndex) => {
|
|
3561
|
+
const observed = await applyStepAssertions(step, page, assertions, assertionContexts, pageIndex);
|
|
3562
|
+
assertionResults.push(...observed);
|
|
3563
|
+
if (observed.some((assertion) => !assertion.passed)) assertionsFailed = true;
|
|
3564
|
+
};
|
|
3556
3565
|
if (routingContext) {
|
|
3557
3566
|
let probes = routedPriorProbes.get(assignment.instanceIndex);
|
|
3558
3567
|
if (!probes) {
|
|
@@ -3670,45 +3679,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
3670
3679
|
}
|
|
3671
3680
|
stepResults.push(result2);
|
|
3672
3681
|
priorStepResults.set(step.id, result2);
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
for (const spec of assertions) {
|
|
3678
|
-
if (!spec.onStep) continue;
|
|
3679
|
-
if (stepDisablesAssertion(step.invariants, spec.id)) continue;
|
|
3680
|
-
if (schemaInvalidResponse) {
|
|
3681
|
-
result2.validations.push({
|
|
3682
|
-
check: "assertion",
|
|
3683
|
-
passed: true,
|
|
3684
|
-
description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
|
|
3685
|
-
});
|
|
3686
|
-
continue;
|
|
3687
|
-
}
|
|
3688
|
-
const raw = await spec.onStep(assertionContexts.get(spec.id), {
|
|
3689
|
-
...result2,
|
|
3690
|
-
context: withoutProtectedCaptures(result2.context ?? {})
|
|
3691
|
-
});
|
|
3692
|
-
for (const r of raw) {
|
|
3693
|
-
const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
|
|
3694
|
-
assertionResults.push(full);
|
|
3695
|
-
if (!rateLimitTripNotApplicable) {
|
|
3696
|
-
result2.validations.push({
|
|
3697
|
-
check: "assertion",
|
|
3698
|
-
passed: r.passed,
|
|
3699
|
-
description: `${spec.id}: ${r.description}`,
|
|
3700
|
-
...r.error !== void 0 && { error: r.error }
|
|
3701
|
-
});
|
|
3702
|
-
}
|
|
3703
|
-
if (r.hint) {
|
|
3704
|
-
const existing = result2.hints ?? [];
|
|
3705
|
-
result2.hints = [...existing, r.hint];
|
|
3706
|
-
}
|
|
3707
|
-
if (!r.passed) {
|
|
3708
|
-
if (!rateLimitTripNotApplicable) result2.passed = false;
|
|
3709
|
-
assertionsFailed = true;
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3682
|
+
if (!result2.product_page_collection) {
|
|
3683
|
+
const observedAssertions = await applyStepAssertions(step, result2, assertions, assertionContexts);
|
|
3684
|
+
assertionResults.push(...observedAssertions);
|
|
3685
|
+
if (observedAssertions.some((assertion) => !assertion.passed)) assertionsFailed = true;
|
|
3712
3686
|
}
|
|
3713
3687
|
if (step.task === "protected_resource_metadata") {
|
|
3714
3688
|
if (result2.skipped && result2.skip_reason === "oauth_not_advertised") {
|
|
@@ -4890,6 +4864,18 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
|
|
|
4890
4864
|
}
|
|
4891
4865
|
}
|
|
4892
4866
|
}
|
|
4867
|
+
if (found.step.collect_product_pages) {
|
|
4868
|
+
const pageAssertions = resolveAssertions(storyboard.invariants);
|
|
4869
|
+
const pageContexts = /* @__PURE__ */ new Map();
|
|
4870
|
+
for (const spec of pageAssertions) {
|
|
4871
|
+
const assertionContext = { storyboard, agentUrl, options, state: {} };
|
|
4872
|
+
pageContexts.set(spec.id, assertionContext);
|
|
4873
|
+
if (spec.onStart) await spec.onStart(assertionContext);
|
|
4874
|
+
}
|
|
4875
|
+
runState.onCollectedProductPage = async (page, pageIndex) => {
|
|
4876
|
+
await applyStepAssertions(found.step, page, pageAssertions, pageContexts, pageIndex);
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4893
4879
|
const result = prerequisiteFailure ?? await executeStep(client, found.step, storyboard.id, found.phaseId, context, allSteps, options, runState);
|
|
4894
4880
|
if (!result.skipped && result.passed && found.step.contributes_to) {
|
|
4895
4881
|
if (evalContributesIf(found.step.contributes_if, /* @__PURE__ */ new Map())) {
|
|
@@ -4908,6 +4894,82 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
|
|
|
4908
4894
|
]);
|
|
4909
4895
|
}
|
|
4910
4896
|
}
|
|
4897
|
+
async function applyStepAssertions(step, result, assertions, assertionContexts, pageIndex) {
|
|
4898
|
+
const findings = [];
|
|
4899
|
+
const schemaInvalid = result.validations.some((v) => v.check === "response_schema" && validationFailsStep(v));
|
|
4900
|
+
const rateSkip = result.skipped === true && result.skip_reason === "rate_limit_not_triggered";
|
|
4901
|
+
for (const spec of assertions) {
|
|
4902
|
+
if (!spec.onStep || stepDisablesAssertion(step.invariants, spec.id)) continue;
|
|
4903
|
+
if (schemaInvalid) {
|
|
4904
|
+
result.validations.push({
|
|
4905
|
+
check: "assertion",
|
|
4906
|
+
passed: true,
|
|
4907
|
+
description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
|
|
4908
|
+
});
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4911
|
+
const raw = await spec.onStep(assertionContexts.get(spec.id), {
|
|
4912
|
+
...result,
|
|
4913
|
+
context: withoutProtectedCaptures(result.context ?? {})
|
|
4914
|
+
});
|
|
4915
|
+
for (const observed of raw) {
|
|
4916
|
+
const description = `${pageIndex === void 0 ? "" : `[physical page ${pageIndex}] `}${observed.description}`;
|
|
4917
|
+
findings.push({ ...observed, description, assertion_id: spec.id, scope: "step", step_id: step.id });
|
|
4918
|
+
if (!rateSkip)
|
|
4919
|
+
result.validations.push({
|
|
4920
|
+
check: "assertion",
|
|
4921
|
+
passed: observed.passed,
|
|
4922
|
+
description: `${spec.id}: ${description}`,
|
|
4923
|
+
...observed.error !== void 0 && { error: observed.error },
|
|
4924
|
+
...pageIndex !== void 0 && !observed.passed && { request: result.request, response: result.response_record ?? null }
|
|
4925
|
+
});
|
|
4926
|
+
if (observed.hint) result.hints = [...result.hints ?? [], observed.hint];
|
|
4927
|
+
if (!observed.passed && !rateSkip) result.passed = false;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
return findings;
|
|
4931
|
+
}
|
|
4932
|
+
function productPageStepResult(step, phaseId, context, page, schemaVersion) {
|
|
4933
|
+
const failedSchema = page.schemaFailure;
|
|
4934
|
+
return {
|
|
4935
|
+
step_id: step.id,
|
|
4936
|
+
phase_id: phaseId,
|
|
4937
|
+
task: "get_products",
|
|
4938
|
+
title: step.title,
|
|
4939
|
+
passed: page.schema_validated && page.result?.success === true,
|
|
4940
|
+
duration_ms: page.response?.duration_ms ?? 0,
|
|
4941
|
+
request: page.request,
|
|
4942
|
+
...page.response && { response_record: page.response },
|
|
4943
|
+
response: redactSecrets(page.result?.data),
|
|
4944
|
+
extraction: page.extraction,
|
|
4945
|
+
context,
|
|
4946
|
+
validations: failedSchema ? [
|
|
4947
|
+
{
|
|
4948
|
+
check: "response_schema",
|
|
4949
|
+
passed: false,
|
|
4950
|
+
description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
|
|
4951
|
+
error: failedSchema.detail,
|
|
4952
|
+
json_pointer: null,
|
|
4953
|
+
expected: failedSchema.schema_id,
|
|
4954
|
+
actual: "schema_validation_failed",
|
|
4955
|
+
schema_id: failedSchema.schema_id,
|
|
4956
|
+
schema_url: failedSchema.schema_url,
|
|
4957
|
+
request: page.request,
|
|
4958
|
+
response: page.response,
|
|
4959
|
+
strict: { valid: false, variant: "sync" }
|
|
4960
|
+
}
|
|
4961
|
+
] : page.schema_validated ? [
|
|
4962
|
+
{
|
|
4963
|
+
check: "response_schema",
|
|
4964
|
+
passed: true,
|
|
4965
|
+
description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
|
|
4966
|
+
schema_id: `/schemas/${schemaVersion}/media-buy/get-products-response.json`,
|
|
4967
|
+
schema_url: `https://adcontextprotocol.org/schemas/${schemaVersion}/media-buy/get-products-response.json`,
|
|
4968
|
+
...page.strict_schema_validated && { strict: { valid: true, variant: "sync" } }
|
|
4969
|
+
}
|
|
4970
|
+
] : []
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4911
4973
|
function unresolvedStepContextVars(step, request, context, runState) {
|
|
4912
4974
|
const unavailableInputs = (step.context_inputs ?? []).filter(
|
|
4913
4975
|
(input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
|
|
@@ -5360,6 +5422,34 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5360
5422
|
};
|
|
5361
5423
|
}
|
|
5362
5424
|
const useRawProbe = rawProbeHeaders !== void 0 && !useSdkAuthOverride;
|
|
5425
|
+
if (step.collect_product_pages && (useRawProbe || request.buying_mode !== "wholesale" || Object.hasOwn(request, "idempotency_key") || request.pagination !== null && typeof request.pagination === "object" && "cursor" in request.pagination || ["if_none_match", "if_wholesale_feed_version", "if_pricing_version"].some((field) => request[field] !== void 0))) {
|
|
5426
|
+
const detail = "collect_product_pages requires fresh unkeyed reads and an unconditional wholesale first page through the official SDK client";
|
|
5427
|
+
return {
|
|
5428
|
+
step_id: step.id,
|
|
5429
|
+
phase_id: phaseId,
|
|
5430
|
+
task: step.task,
|
|
5431
|
+
title: step.title,
|
|
5432
|
+
passed: false,
|
|
5433
|
+
duration_ms: 0,
|
|
5434
|
+
context,
|
|
5435
|
+
extraction: { path: "none" },
|
|
5436
|
+
error: detail,
|
|
5437
|
+
validations: [
|
|
5438
|
+
{
|
|
5439
|
+
check: "product_page_collection",
|
|
5440
|
+
passed: false,
|
|
5441
|
+
description: detail,
|
|
5442
|
+
json_pointer: null,
|
|
5443
|
+
expected: "unkeyed unconditional wholesale query",
|
|
5444
|
+
actual: redactSecrets(request),
|
|
5445
|
+
schema_id: null,
|
|
5446
|
+
schema_url: null,
|
|
5447
|
+
request: null,
|
|
5448
|
+
response: null
|
|
5449
|
+
}
|
|
5450
|
+
]
|
|
5451
|
+
};
|
|
5452
|
+
}
|
|
5363
5453
|
const stepClient = () => authOverrideClient ?? client;
|
|
5364
5454
|
let taskResult;
|
|
5365
5455
|
let stepResult;
|
|
@@ -5369,6 +5459,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5369
5459
|
let requestUrl;
|
|
5370
5460
|
let a2aEnvelope;
|
|
5371
5461
|
let crossResponses;
|
|
5462
|
+
let productPageCollection;
|
|
5463
|
+
let collectedPageValidations = [];
|
|
5464
|
+
let actualCollectedResponse;
|
|
5465
|
+
let collectedRequestRecord;
|
|
5372
5466
|
if (step.parallel_dispatch && !useRawProbe) {
|
|
5373
5467
|
const contractsInScope = new Set(options.contracts ?? []);
|
|
5374
5468
|
if (!contractsInScope.has(PARALLEL_DISPATCH_CONTRACT)) {
|
|
@@ -5537,7 +5631,96 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5537
5631
|
} else {
|
|
5538
5632
|
const captureA2a = options.protocol === "a2a";
|
|
5539
5633
|
let a2aCaptures;
|
|
5540
|
-
if (step.
|
|
5634
|
+
if (step.collect_product_pages) {
|
|
5635
|
+
const started = Date.now();
|
|
5636
|
+
const pages = [];
|
|
5637
|
+
const pageValidations = [];
|
|
5638
|
+
let pagesPassed = true;
|
|
5639
|
+
const collection = await collectWholesaleProductPages(stepClient(), options, request, {
|
|
5640
|
+
retainRestartEvidence: true,
|
|
5641
|
+
dispatch: (pageRequest) => executeStoryboardTask(stepClient(), effectiveStep.task, pageRequest, {
|
|
5642
|
+
skipIdempotencyAutoInject: testsIdempotencyKeyOmission,
|
|
5643
|
+
skipAccountValidation: testsMissingAccount,
|
|
5644
|
+
skipRequestValidation: testsSchemaInvalidRequest,
|
|
5645
|
+
preserveGovernedPayload: governedRequest,
|
|
5646
|
+
responseProjection,
|
|
5647
|
+
mediaBuyLifecycleCompatibility: options.mediaBuyLifecycleCompatibility,
|
|
5648
|
+
signal: options.signal
|
|
5649
|
+
}),
|
|
5650
|
+
onPage: async (observation) => {
|
|
5651
|
+
const physical = productPageStepResult(
|
|
5652
|
+
step,
|
|
5653
|
+
phaseId,
|
|
5654
|
+
context,
|
|
5655
|
+
observation,
|
|
5656
|
+
options._serverAdcpVersion ?? options.adcpVersion ?? ADCP_VERSION
|
|
5657
|
+
);
|
|
5658
|
+
await runState.onCollectedProductPage?.(physical, observation.page_index);
|
|
5659
|
+
pagesPassed &&= physical.passed;
|
|
5660
|
+
pageValidations.push(...physical.validations);
|
|
5661
|
+
const page = {
|
|
5662
|
+
page_index: observation.page_index,
|
|
5663
|
+
restart_count: observation.restart_count,
|
|
5664
|
+
request: observation.request,
|
|
5665
|
+
response: observation.response,
|
|
5666
|
+
extraction: observation.extraction,
|
|
5667
|
+
schema_validated: observation.schema_validated
|
|
5668
|
+
};
|
|
5669
|
+
if (pages.length >= 16 && page.response) {
|
|
5670
|
+
page.response = {
|
|
5671
|
+
...page.response,
|
|
5672
|
+
payload: {
|
|
5673
|
+
response_not_retained: "Physical page was checked; response evidence exceeded the 16-page retention budget."
|
|
5674
|
+
}
|
|
5675
|
+
};
|
|
5676
|
+
}
|
|
5677
|
+
pages.push(page);
|
|
5678
|
+
}
|
|
5679
|
+
});
|
|
5680
|
+
if (options.signal?.aborted) options.signal.throwIfAborted();
|
|
5681
|
+
const logical = collection.ok ? {
|
|
5682
|
+
...collection.catalog.firstPage.result.data,
|
|
5683
|
+
products: collection.catalog.products,
|
|
5684
|
+
...collection.catalog.errors.length > 0 && { errors: collection.catalog.errors }
|
|
5685
|
+
} : void 0;
|
|
5686
|
+
if (logical) delete logical.pagination;
|
|
5687
|
+
productPageCollection = {
|
|
5688
|
+
complete: collection.ok,
|
|
5689
|
+
page_count: pages.length,
|
|
5690
|
+
restart_count: pages.reduce((maximum, page) => Math.max(maximum, page.restart_count), 0),
|
|
5691
|
+
pages,
|
|
5692
|
+
...logical && { logical_response: redactSecrets(logical) }
|
|
5693
|
+
};
|
|
5694
|
+
collectedRequestRecord = collection.ok ? collection.catalog.firstPage.request : collection.exchange?.request;
|
|
5695
|
+
const observedResponse = collection.ok ? collection.catalog.firstPage.response : collection.exchange?.response;
|
|
5696
|
+
responseRecord = observedResponse ?? void 0;
|
|
5697
|
+
actualCollectedResponse = collection.ok ? collection.catalog.firstPage.result.data : collection.exchange?.response?.payload;
|
|
5698
|
+
taskResult = collection.ok ? { ...collection.catalog.firstPage.result, data: logical } : {
|
|
5699
|
+
success: false,
|
|
5700
|
+
error: collection.error.replace(/^fixture discovery/, "Product page collection")
|
|
5701
|
+
};
|
|
5702
|
+
stepResult = {
|
|
5703
|
+
duration_ms: Date.now() - started,
|
|
5704
|
+
passed: collection.ok && pagesPassed,
|
|
5705
|
+
...!collection.ok && { error: collection.error.replace(/^fixture discovery/, "Product page collection") }
|
|
5706
|
+
};
|
|
5707
|
+
collectedPageValidations = pageValidations;
|
|
5708
|
+
if (!collection.ok && !pageValidations.some((validation) => validationFailsStep(validation))) {
|
|
5709
|
+
collectedPageValidations.push({
|
|
5710
|
+
check: "product_page_collection",
|
|
5711
|
+
passed: false,
|
|
5712
|
+
description: "Wholesale collection completes one stable, duplicate-free feed",
|
|
5713
|
+
error: collection.error,
|
|
5714
|
+
json_pointer: null,
|
|
5715
|
+
expected: "a complete stable wholesale collection",
|
|
5716
|
+
actual: collection.error,
|
|
5717
|
+
schema_id: null,
|
|
5718
|
+
schema_url: null,
|
|
5719
|
+
...collection.exchange?.request && { request: collection.exchange.request },
|
|
5720
|
+
response: collection.exchange?.response ?? null
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
} else if (step.parallel_dispatch) {
|
|
5541
5724
|
const started = Date.now();
|
|
5542
5725
|
crossResponses = await runParallelDispatches(stepClient(), effectiveStep.task, request, {
|
|
5543
5726
|
spec: step.parallel_dispatch,
|
|
@@ -5625,7 +5808,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5625
5808
|
const requestRecord = {
|
|
5626
5809
|
transport: options.protocol === "a2a" ? "a2a" : "mcp",
|
|
5627
5810
|
operation: effectiveStep.task,
|
|
5628
|
-
payload: redactSecrets(request),
|
|
5811
|
+
payload: collectedRequestRecord?.payload ?? redactSecrets(request),
|
|
5629
5812
|
...effectiveRequestUrl ? { url: redactOAuthUrlForOutput(effectiveRequestUrl) } : {}
|
|
5630
5813
|
};
|
|
5631
5814
|
const inputSchemaStripNotices = collectInputSchemaFieldStripNotices(
|
|
@@ -5813,9 +5996,9 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5813
5996
|
success: true,
|
|
5814
5997
|
data: schemaValidationError.data
|
|
5815
5998
|
} : void 0);
|
|
5816
|
-
let validations = [];
|
|
5999
|
+
let validations = [...collectedPageValidations];
|
|
5817
6000
|
if (step.validations?.length && (validationTaskResult || httpResult)) {
|
|
5818
|
-
const resolvedValidations = step.validations.map((v) => {
|
|
6001
|
+
const resolvedValidations = step.validations.filter((v) => !productPageCollection || v.check !== "response_schema").map((v) => {
|
|
5819
6002
|
const resolved = { ...v };
|
|
5820
6003
|
if (resolved.value !== void 0) {
|
|
5821
6004
|
resolved.value = injectContext({ __v: resolved.value }, context, runState.runnerVars).__v;
|
|
@@ -5872,9 +6055,11 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5872
6055
|
};
|
|
5873
6056
|
})()
|
|
5874
6057
|
};
|
|
5875
|
-
validations
|
|
5876
|
-
|
|
5877
|
-
|
|
6058
|
+
validations.push(
|
|
6059
|
+
...runValidations(
|
|
6060
|
+
resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
|
|
6061
|
+
vctx
|
|
6062
|
+
)
|
|
5878
6063
|
);
|
|
5879
6064
|
for (const validation of resolvedValidations.filter((validation2) => validation2.check === "property_list_reference")) {
|
|
5880
6065
|
const result = effectiveStep.task !== "create_property_list" || !validationTaskResult?.success ? {
|
|
@@ -6144,6 +6329,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6144
6329
|
}
|
|
6145
6330
|
}
|
|
6146
6331
|
}
|
|
6332
|
+
if (productPageCollection) {
|
|
6333
|
+
for (const validation of validations) {
|
|
6334
|
+
if (collectedPageValidations.includes(validation)) continue;
|
|
6335
|
+
if (validation.json_pointer)
|
|
6336
|
+
validation.json_pointer = `/product_page_collection/logical_response${validation.json_pointer}`;
|
|
6337
|
+
validation.description = `[logical product collection] ${validation.description ?? validation.check}`;
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6147
6340
|
if (protectedViolations.length > 0) validations.push(...protectedViolations);
|
|
6148
6341
|
const allValidationsPassedFinal = validations.every((v) => !validationFailsStep(v));
|
|
6149
6342
|
const stepFailed = !(passed && allValidationsPassedFinal);
|
|
@@ -6173,7 +6366,8 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6173
6366
|
// Legacy `response` field (new code reads `response_record`).
|
|
6174
6367
|
// Redact in case a downstream consumer still keys off it; the
|
|
6175
6368
|
// modern `response_record.payload` path is already redacted.
|
|
6176
|
-
response: redactSecrets(taskResult?.data),
|
|
6369
|
+
response: redactSecrets(productPageCollection ? actualCollectedResponse : taskResult?.data),
|
|
6370
|
+
...productPageCollection && { product_page_collection: productPageCollection },
|
|
6177
6371
|
validations,
|
|
6178
6372
|
// NOT projected here: the full runner threads this map from step to
|
|
6179
6373
|
// step, so stripping the reserved namespace would break the very
|
|
@@ -6197,7 +6391,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6197
6391
|
next,
|
|
6198
6392
|
request: requestRecord,
|
|
6199
6393
|
...responseRecord && { response_record: responseRecord },
|
|
6200
|
-
extraction:
|
|
6394
|
+
extraction: productPageCollection ? {
|
|
6395
|
+
path: "none",
|
|
6396
|
+
note: "Authored checks and context use product_page_collection.logical_response; response_record remains an actual physical page."
|
|
6397
|
+
} : extractionFromTaskResult(taskResult),
|
|
6201
6398
|
...inputSchemaStripNotices.length > 0 && { notices: inputSchemaStripNotices },
|
|
6202
6399
|
...hints.length > 0 && { hints }
|
|
6203
6400
|
};
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
* `controller_seeding_failed`.
|
|
22
22
|
*/
|
|
23
23
|
import { type TestClient } from '../client.mjs';
|
|
24
|
+
import type { TaskResult } from '../types.mjs';
|
|
25
|
+
import type { DebugLogEntry } from '../../types/adcp.mjs';
|
|
24
26
|
import { FixtureBindingRegistry } from './fixture-resolution.mjs';
|
|
25
27
|
import { type CollectionSelectionWitness } from './collection-selection.mjs';
|
|
26
|
-
import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
|
|
28
|
+
import type { ContextProvenanceEntry, RunnerExtractionRecord, RunnerRequestRecord, RunnerResponseRecord, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
|
|
27
29
|
/** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
|
|
28
30
|
export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
|
|
29
31
|
/** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
|
|
@@ -101,4 +103,87 @@ export declare function runControllerSeeding(client: TestClient, storyboard: Sto
|
|
|
101
103
|
client: TestClient;
|
|
102
104
|
options: StoryboardRunOptions;
|
|
103
105
|
}): Promise<ControllerSeedingResult | null>;
|
|
106
|
+
/**
|
|
107
|
+
* One actual `get_products` exchange from the discovery ladder.
|
|
108
|
+
*
|
|
109
|
+
* `runner-output-contract.yaml` makes `request` and `response` required on
|
|
110
|
+
* a failing validation, with null reserved for pre-wire failures. A capture
|
|
111
|
+
* that cannot be built from a pinned entity is NOT pre-wire — the entity
|
|
112
|
+
* came off the wire — so the failing result has to carry the exchange that
|
|
113
|
+
* produced it rather than an explicit "no contact" null.
|
|
114
|
+
*/
|
|
115
|
+
export interface DiscoveryExchange {
|
|
116
|
+
request: RunnerRequestRecord;
|
|
117
|
+
response: RunnerResponseRecord;
|
|
118
|
+
}
|
|
119
|
+
export interface DiscoveryCatalog {
|
|
120
|
+
products: Array<Record<string, unknown>>;
|
|
121
|
+
firstPage: ProductPageObservation;
|
|
122
|
+
errors: unknown[];
|
|
123
|
+
evidence: {
|
|
124
|
+
page_count: number;
|
|
125
|
+
product_count: number;
|
|
126
|
+
cursors: string[];
|
|
127
|
+
restart_count?: number;
|
|
128
|
+
};
|
|
129
|
+
/** Per-page exchanges, in request order. */
|
|
130
|
+
exchanges: DiscoveryExchange[];
|
|
131
|
+
/** product_id → index into `exchanges`, so a failure cites ITS page. */
|
|
132
|
+
pageOfProduct: Map<string, number>;
|
|
133
|
+
}
|
|
134
|
+
export type DiscoveryCatalogResult = {
|
|
135
|
+
ok: true;
|
|
136
|
+
catalog: DiscoveryCatalog;
|
|
137
|
+
} | {
|
|
138
|
+
ok: false;
|
|
139
|
+
error: string;
|
|
140
|
+
evidence?: unknown;
|
|
141
|
+
/**
|
|
142
|
+
* The actual exchange that failed, so the producer step can record
|
|
143
|
+
* truthful wire evidence instead of nothing. `response` is null only
|
|
144
|
+
* when the transport threw and no response was ever received — never
|
|
145
|
+
* a stand-in for one we did not capture.
|
|
146
|
+
*/
|
|
147
|
+
exchange?: {
|
|
148
|
+
request: RunnerRequestRecord;
|
|
149
|
+
response: RunnerResponseRecord | null;
|
|
150
|
+
extraction?: RunnerExtractionRecord;
|
|
151
|
+
};
|
|
152
|
+
/** AJV detail when the page parsed but failed its response schema. */
|
|
153
|
+
unavailable?: true;
|
|
154
|
+
schemaFailure?: {
|
|
155
|
+
detail: string;
|
|
156
|
+
schema_id: string;
|
|
157
|
+
schema_url: string;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** One actual page, observed before any restart can discard its products. */
|
|
161
|
+
type ProductPageTaskResult = TaskResult & {
|
|
162
|
+
debug_logs?: DebugLogEntry[];
|
|
163
|
+
};
|
|
164
|
+
export interface ProductPageObservation {
|
|
165
|
+
page_index: number;
|
|
166
|
+
restart_count: number;
|
|
167
|
+
request: RunnerRequestRecord;
|
|
168
|
+
response: RunnerResponseRecord | null;
|
|
169
|
+
extraction: RunnerExtractionRecord;
|
|
170
|
+
result?: ProductPageTaskResult;
|
|
171
|
+
schema_validated: boolean;
|
|
172
|
+
/** A cached JSON-schema validator, rather than the legacy fallback, checked this page. */
|
|
173
|
+
strict_schema_validated?: boolean;
|
|
174
|
+
schemaFailure?: {
|
|
175
|
+
detail: string;
|
|
176
|
+
schema_id: string;
|
|
177
|
+
schema_url: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export interface ProductPageCollectionControls {
|
|
181
|
+
/** Preserve the normal step's execution policy and actual authored arguments. */
|
|
182
|
+
dispatch?: (request: Record<string, unknown>) => Promise<ProductPageTaskResult>;
|
|
183
|
+
/** Runs for every observed exchange, including pages later abandoned on restart. */
|
|
184
|
+
onPage?: (page: ProductPageObservation) => Promise<void>;
|
|
185
|
+
retainRestartEvidence?: boolean;
|
|
186
|
+
}
|
|
187
|
+
/** Shared strict unkeyed wholesale walker; callers own the initial query and logical view. */
|
|
188
|
+
export declare function collectWholesaleProductPages(client: TestClient, options: StoryboardRunOptions, query: Record<string, unknown>, controls?: ProductPageCollectionControls): Promise<DiscoveryCatalogResult>;
|
|
104
189
|
export {};
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
* `controller_seeding_failed`.
|
|
22
22
|
*/
|
|
23
23
|
import { type TestClient } from '../client';
|
|
24
|
+
import type { TaskResult } from '../types';
|
|
25
|
+
import type { DebugLogEntry } from '../../types/adcp';
|
|
24
26
|
import { FixtureBindingRegistry } from './fixture-resolution';
|
|
25
27
|
import { type CollectionSelectionWitness } from './collection-selection';
|
|
26
|
-
import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
|
|
28
|
+
import type { ContextProvenanceEntry, RunnerExtractionRecord, RunnerRequestRecord, RunnerResponseRecord, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
|
|
27
29
|
/** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
|
|
28
30
|
export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
|
|
29
31
|
/** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
|
|
@@ -101,4 +103,87 @@ export declare function runControllerSeeding(client: TestClient, storyboard: Sto
|
|
|
101
103
|
client: TestClient;
|
|
102
104
|
options: StoryboardRunOptions;
|
|
103
105
|
}): Promise<ControllerSeedingResult | null>;
|
|
106
|
+
/**
|
|
107
|
+
* One actual `get_products` exchange from the discovery ladder.
|
|
108
|
+
*
|
|
109
|
+
* `runner-output-contract.yaml` makes `request` and `response` required on
|
|
110
|
+
* a failing validation, with null reserved for pre-wire failures. A capture
|
|
111
|
+
* that cannot be built from a pinned entity is NOT pre-wire — the entity
|
|
112
|
+
* came off the wire — so the failing result has to carry the exchange that
|
|
113
|
+
* produced it rather than an explicit "no contact" null.
|
|
114
|
+
*/
|
|
115
|
+
export interface DiscoveryExchange {
|
|
116
|
+
request: RunnerRequestRecord;
|
|
117
|
+
response: RunnerResponseRecord;
|
|
118
|
+
}
|
|
119
|
+
export interface DiscoveryCatalog {
|
|
120
|
+
products: Array<Record<string, unknown>>;
|
|
121
|
+
firstPage: ProductPageObservation;
|
|
122
|
+
errors: unknown[];
|
|
123
|
+
evidence: {
|
|
124
|
+
page_count: number;
|
|
125
|
+
product_count: number;
|
|
126
|
+
cursors: string[];
|
|
127
|
+
restart_count?: number;
|
|
128
|
+
};
|
|
129
|
+
/** Per-page exchanges, in request order. */
|
|
130
|
+
exchanges: DiscoveryExchange[];
|
|
131
|
+
/** product_id → index into `exchanges`, so a failure cites ITS page. */
|
|
132
|
+
pageOfProduct: Map<string, number>;
|
|
133
|
+
}
|
|
134
|
+
export type DiscoveryCatalogResult = {
|
|
135
|
+
ok: true;
|
|
136
|
+
catalog: DiscoveryCatalog;
|
|
137
|
+
} | {
|
|
138
|
+
ok: false;
|
|
139
|
+
error: string;
|
|
140
|
+
evidence?: unknown;
|
|
141
|
+
/**
|
|
142
|
+
* The actual exchange that failed, so the producer step can record
|
|
143
|
+
* truthful wire evidence instead of nothing. `response` is null only
|
|
144
|
+
* when the transport threw and no response was ever received — never
|
|
145
|
+
* a stand-in for one we did not capture.
|
|
146
|
+
*/
|
|
147
|
+
exchange?: {
|
|
148
|
+
request: RunnerRequestRecord;
|
|
149
|
+
response: RunnerResponseRecord | null;
|
|
150
|
+
extraction?: RunnerExtractionRecord;
|
|
151
|
+
};
|
|
152
|
+
/** AJV detail when the page parsed but failed its response schema. */
|
|
153
|
+
unavailable?: true;
|
|
154
|
+
schemaFailure?: {
|
|
155
|
+
detail: string;
|
|
156
|
+
schema_id: string;
|
|
157
|
+
schema_url: string;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** One actual page, observed before any restart can discard its products. */
|
|
161
|
+
type ProductPageTaskResult = TaskResult & {
|
|
162
|
+
debug_logs?: DebugLogEntry[];
|
|
163
|
+
};
|
|
164
|
+
export interface ProductPageObservation {
|
|
165
|
+
page_index: number;
|
|
166
|
+
restart_count: number;
|
|
167
|
+
request: RunnerRequestRecord;
|
|
168
|
+
response: RunnerResponseRecord | null;
|
|
169
|
+
extraction: RunnerExtractionRecord;
|
|
170
|
+
result?: ProductPageTaskResult;
|
|
171
|
+
schema_validated: boolean;
|
|
172
|
+
/** A cached JSON-schema validator, rather than the legacy fallback, checked this page. */
|
|
173
|
+
strict_schema_validated?: boolean;
|
|
174
|
+
schemaFailure?: {
|
|
175
|
+
detail: string;
|
|
176
|
+
schema_id: string;
|
|
177
|
+
schema_url: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export interface ProductPageCollectionControls {
|
|
181
|
+
/** Preserve the normal step's execution policy and actual authored arguments. */
|
|
182
|
+
dispatch?: (request: Record<string, unknown>) => Promise<ProductPageTaskResult>;
|
|
183
|
+
/** Runs for every observed exchange, including pages later abandoned on restart. */
|
|
184
|
+
onPage?: (page: ProductPageObservation) => Promise<void>;
|
|
185
|
+
retainRestartEvidence?: boolean;
|
|
186
|
+
}
|
|
187
|
+
/** Shared strict unkeyed wholesale walker; callers own the initial query and logical view. */
|
|
188
|
+
export declare function collectWholesaleProductPages(client: TestClient, options: StoryboardRunOptions, query: Record<string, unknown>, controls?: ProductPageCollectionControls): Promise<DiscoveryCatalogResult>;
|
|
104
189
|
export {};
|