@adcp/sdk 17.0.0-beta.6 → 17.0.0-beta.7
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/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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const CONTAINERS = {
|
|
2
|
+
viewable_rate: "viewability",
|
|
3
|
+
viewable_impressions: "viewability",
|
|
4
|
+
measurable_impressions: "viewability",
|
|
5
|
+
viewed_seconds: "viewability",
|
|
6
|
+
quartile_25: "quartile_data",
|
|
7
|
+
quartile_50: "quartile_data",
|
|
8
|
+
quartile_75: "quartile_data",
|
|
9
|
+
quartile_100: "quartile_data"
|
|
10
|
+
};
|
|
11
|
+
const DERIVED_FROM = {
|
|
12
|
+
ctr: ["clicks", "impressions"],
|
|
13
|
+
completion_rate: ["completed_views", "impressions"],
|
|
14
|
+
roas: ["conversion_value", "spend"],
|
|
15
|
+
cost_per_acquisition: ["spend", "conversions"],
|
|
16
|
+
cost_per_click: ["spend", "clicks"],
|
|
17
|
+
cost_per_completed_view: ["spend", "completed_views"],
|
|
18
|
+
cpm: ["spend", "impressions"],
|
|
19
|
+
viewable_rate: ["viewable_impressions", "measurable_impressions"]
|
|
20
|
+
};
|
|
21
|
+
function record(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
23
|
+
}
|
|
24
|
+
function metricAvailable(metric, declared) {
|
|
25
|
+
if (declared.has(metric)) return true;
|
|
26
|
+
const container = Object.hasOwn(CONTAINERS, metric) ? CONTAINERS[metric] : void 0;
|
|
27
|
+
if (container && declared.has(container)) return true;
|
|
28
|
+
const derivation = Object.hasOwn(DERIVED_FROM, metric) ? DERIVED_FROM[metric] : void 0;
|
|
29
|
+
return derivation?.every((dependency) => metricAvailable(dependency, declared)) ?? false;
|
|
30
|
+
}
|
|
31
|
+
function vendorMetricMatches(actual, required) {
|
|
32
|
+
const entry = record(actual);
|
|
33
|
+
if (!entry || required.metric_id !== void 0 && entry.metric_id !== required.metric_id) return false;
|
|
34
|
+
const vendor = record(required.vendor);
|
|
35
|
+
if (!vendor) return true;
|
|
36
|
+
const actualVendor = record(entry.vendor);
|
|
37
|
+
if (!actualVendor) return false;
|
|
38
|
+
return actualVendor.domain === vendor.domain && (vendor.brand_id === void 0 || actualVendor.brand_id === vendor.brand_id);
|
|
39
|
+
}
|
|
40
|
+
const requiredMetricFiltersAssertion = {
|
|
41
|
+
id: "discovery.required_metric_filters",
|
|
42
|
+
default: true,
|
|
43
|
+
description: "Every returned product satisfies all requested standard and vendor metric filters.",
|
|
44
|
+
onStep(_ctx, step) {
|
|
45
|
+
if (step.skipped || step.task !== "get_products") return [];
|
|
46
|
+
const request = record(step.request?.payload);
|
|
47
|
+
const filters = record(request?.filters);
|
|
48
|
+
const required = Array.isArray(filters?.required_metrics) ? filters.required_metrics.filter((metric) => typeof metric === "string") : [];
|
|
49
|
+
const requiredVendors = Array.isArray(filters?.required_vendor_metrics) ? filters.required_vendor_metrics.flatMap((value) => {
|
|
50
|
+
const entry = record(value);
|
|
51
|
+
return entry && (typeof entry.metric_id === "string" || typeof record(entry.vendor)?.domain === "string") ? [entry] : [];
|
|
52
|
+
}) : [];
|
|
53
|
+
if (required.length === 0 && requiredVendors.length === 0) return [];
|
|
54
|
+
const raw = record(step.response ?? step.response_record?.payload);
|
|
55
|
+
const response = Array.isArray(raw?.products) ? raw : record(raw?.data) ?? raw;
|
|
56
|
+
if (!Array.isArray(response?.products) || response.products.length === 0) return [];
|
|
57
|
+
const reportingProjectedAway = Array.isArray(request?.fields) && !request.fields.includes("reporting_capabilities");
|
|
58
|
+
const findings = [];
|
|
59
|
+
for (const [index, value] of response.products.entries()) {
|
|
60
|
+
const product = record(value);
|
|
61
|
+
if (!product) continue;
|
|
62
|
+
if (reportingProjectedAway && product.reporting_capabilities === void 0) {
|
|
63
|
+
findings.push({
|
|
64
|
+
passed: true,
|
|
65
|
+
status: "not_applicable",
|
|
66
|
+
step_id: step.step_id,
|
|
67
|
+
description: `products[${index}]: required metric filter compatibility is unproven because reporting_capabilities was projected away`
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const capabilities = record(product.reporting_capabilities);
|
|
72
|
+
const declared = /* @__PURE__ */ new Set(["impressions", "spend"]);
|
|
73
|
+
if (Array.isArray(capabilities?.available_metrics)) {
|
|
74
|
+
for (const metric of capabilities.available_metrics) if (typeof metric === "string") declared.add(metric);
|
|
75
|
+
}
|
|
76
|
+
const missing = required.filter((metric) => !metricAvailable(metric, declared));
|
|
77
|
+
if (required.length > 0)
|
|
78
|
+
findings.push({
|
|
79
|
+
passed: missing.length === 0,
|
|
80
|
+
step_id: step.step_id,
|
|
81
|
+
description: `products[${index}] must satisfy every filters.required_metrics entry`,
|
|
82
|
+
...missing.length > 0 && {
|
|
83
|
+
error: `products[${index}] does not declare required metrics ${JSON.stringify(missing)}`
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const vendorMetrics = Array.isArray(capabilities?.vendor_metrics) ? capabilities.vendor_metrics : [];
|
|
87
|
+
for (const [filterIndex, vendor] of requiredVendors.entries()) {
|
|
88
|
+
const matched = vendorMetrics.some((actual) => vendorMetricMatches(actual, vendor));
|
|
89
|
+
findings.push({
|
|
90
|
+
passed: matched,
|
|
91
|
+
step_id: step.step_id,
|
|
92
|
+
description: `products[${index}] must satisfy filters.required_vendor_metrics[${filterIndex}] on one declared vendor metric`,
|
|
93
|
+
...!matched && {
|
|
94
|
+
error: `products[${index}] has no vendor metric matching filters.required_vendor_metrics[${filterIndex}]`
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return findings;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export {
|
|
103
|
+
requiredMetricFiltersAssertion
|
|
104
|
+
};
|
|
@@ -1145,6 +1145,7 @@ function buildPhaseCapabilitySkippedSteps(storyboard, phase, detail, context) {
|
|
|
1145
1145
|
}
|
|
1146
1146
|
const REQUIREMENT_TO_SKIP_REASON = {
|
|
1147
1147
|
controller: "missing_test_controller",
|
|
1148
|
+
collect_product_pages_runner: "requirement_unmet",
|
|
1148
1149
|
seeded_state: "requirement_unmet",
|
|
1149
1150
|
real_wire: "requirement_unmet",
|
|
1150
1151
|
webhook_receiver: "requirement_unmet",
|
|
@@ -1278,6 +1279,8 @@ async function checkRequires(requires, storyboard, options, profile, precomputed
|
|
|
1278
1279
|
continue;
|
|
1279
1280
|
}
|
|
1280
1281
|
switch (requirement) {
|
|
1282
|
+
case "collect_product_pages_runner":
|
|
1283
|
+
break;
|
|
1281
1284
|
case "controller": {
|
|
1282
1285
|
if (!options.agentTools) continue;
|
|
1283
1286
|
if (!options.agentTools.includes("comply_test_controller")) {
|
|
@@ -3511,6 +3514,11 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
3511
3514
|
}
|
|
3512
3515
|
assignment ??= dispatch.nextFor(step);
|
|
3513
3516
|
const stepExecutionState = buildExecutionState(assignment.agentUrl, assignment.profile);
|
|
3517
|
+
stepExecutionState.onCollectedProductPage = async (page, pageIndex) => {
|
|
3518
|
+
const observed = await applyStepAssertions(step, page, assertions, assertionContexts, pageIndex);
|
|
3519
|
+
assertionResults.push(...observed);
|
|
3520
|
+
if (observed.some((assertion) => !assertion.passed)) assertionsFailed = true;
|
|
3521
|
+
};
|
|
3514
3522
|
if (routingContext) {
|
|
3515
3523
|
let probes = routedPriorProbes.get(assignment.instanceIndex);
|
|
3516
3524
|
if (!probes) {
|
|
@@ -3628,45 +3636,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
3628
3636
|
}
|
|
3629
3637
|
stepResults.push(result2);
|
|
3630
3638
|
priorStepResults.set(step.id, result2);
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
for (const spec of assertions) {
|
|
3636
|
-
if (!spec.onStep) continue;
|
|
3637
|
-
if ((0, import_assertions.stepDisablesAssertion)(step.invariants, spec.id)) continue;
|
|
3638
|
-
if (schemaInvalidResponse) {
|
|
3639
|
-
result2.validations.push({
|
|
3640
|
-
check: "assertion",
|
|
3641
|
-
passed: true,
|
|
3642
|
-
description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
|
|
3643
|
-
});
|
|
3644
|
-
continue;
|
|
3645
|
-
}
|
|
3646
|
-
const raw = await spec.onStep(assertionContexts.get(spec.id), {
|
|
3647
|
-
...result2,
|
|
3648
|
-
context: withoutProtectedCaptures(result2.context ?? {})
|
|
3649
|
-
});
|
|
3650
|
-
for (const r of raw) {
|
|
3651
|
-
const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
|
|
3652
|
-
assertionResults.push(full);
|
|
3653
|
-
if (!rateLimitTripNotApplicable) {
|
|
3654
|
-
result2.validations.push({
|
|
3655
|
-
check: "assertion",
|
|
3656
|
-
passed: r.passed,
|
|
3657
|
-
description: `${spec.id}: ${r.description}`,
|
|
3658
|
-
...r.error !== void 0 && { error: r.error }
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
if (r.hint) {
|
|
3662
|
-
const existing = result2.hints ?? [];
|
|
3663
|
-
result2.hints = [...existing, r.hint];
|
|
3664
|
-
}
|
|
3665
|
-
if (!r.passed) {
|
|
3666
|
-
if (!rateLimitTripNotApplicable) result2.passed = false;
|
|
3667
|
-
assertionsFailed = true;
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3639
|
+
if (!result2.product_page_collection) {
|
|
3640
|
+
const observedAssertions = await applyStepAssertions(step, result2, assertions, assertionContexts);
|
|
3641
|
+
assertionResults.push(...observedAssertions);
|
|
3642
|
+
if (observedAssertions.some((assertion) => !assertion.passed)) assertionsFailed = true;
|
|
3670
3643
|
}
|
|
3671
3644
|
if (step.task === "protected_resource_metadata") {
|
|
3672
3645
|
if (result2.skipped && result2.skip_reason === "oauth_not_advertised") {
|
|
@@ -4848,6 +4821,18 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
|
|
|
4848
4821
|
}
|
|
4849
4822
|
}
|
|
4850
4823
|
}
|
|
4824
|
+
if (found.step.collect_product_pages) {
|
|
4825
|
+
const pageAssertions = (0, import_assertions.resolveAssertions)(storyboard.invariants);
|
|
4826
|
+
const pageContexts = /* @__PURE__ */ new Map();
|
|
4827
|
+
for (const spec of pageAssertions) {
|
|
4828
|
+
const assertionContext = { storyboard, agentUrl, options, state: {} };
|
|
4829
|
+
pageContexts.set(spec.id, assertionContext);
|
|
4830
|
+
if (spec.onStart) await spec.onStart(assertionContext);
|
|
4831
|
+
}
|
|
4832
|
+
runState.onCollectedProductPage = async (page, pageIndex) => {
|
|
4833
|
+
await applyStepAssertions(found.step, page, pageAssertions, pageContexts, pageIndex);
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4851
4836
|
const result = prerequisiteFailure ?? await executeStep(client, found.step, storyboard.id, found.phaseId, context, allSteps, options, runState);
|
|
4852
4837
|
if (!result.skipped && result.passed && found.step.contributes_to) {
|
|
4853
4838
|
if (evalContributesIf(found.step.contributes_if, /* @__PURE__ */ new Map())) {
|
|
@@ -4866,6 +4851,82 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
|
|
|
4866
4851
|
]);
|
|
4867
4852
|
}
|
|
4868
4853
|
}
|
|
4854
|
+
async function applyStepAssertions(step, result, assertions, assertionContexts, pageIndex) {
|
|
4855
|
+
const findings = [];
|
|
4856
|
+
const schemaInvalid = result.validations.some((v) => v.check === "response_schema" && (0, import_validations.validationFailsStep)(v));
|
|
4857
|
+
const rateSkip = result.skipped === true && result.skip_reason === "rate_limit_not_triggered";
|
|
4858
|
+
for (const spec of assertions) {
|
|
4859
|
+
if (!spec.onStep || (0, import_assertions.stepDisablesAssertion)(step.invariants, spec.id)) continue;
|
|
4860
|
+
if (schemaInvalid) {
|
|
4861
|
+
result.validations.push({
|
|
4862
|
+
check: "assertion",
|
|
4863
|
+
passed: true,
|
|
4864
|
+
description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
|
|
4865
|
+
});
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
const raw = await spec.onStep(assertionContexts.get(spec.id), {
|
|
4869
|
+
...result,
|
|
4870
|
+
context: withoutProtectedCaptures(result.context ?? {})
|
|
4871
|
+
});
|
|
4872
|
+
for (const observed of raw) {
|
|
4873
|
+
const description = `${pageIndex === void 0 ? "" : `[physical page ${pageIndex}] `}${observed.description}`;
|
|
4874
|
+
findings.push({ ...observed, description, assertion_id: spec.id, scope: "step", step_id: step.id });
|
|
4875
|
+
if (!rateSkip)
|
|
4876
|
+
result.validations.push({
|
|
4877
|
+
check: "assertion",
|
|
4878
|
+
passed: observed.passed,
|
|
4879
|
+
description: `${spec.id}: ${description}`,
|
|
4880
|
+
...observed.error !== void 0 && { error: observed.error },
|
|
4881
|
+
...pageIndex !== void 0 && !observed.passed && { request: result.request, response: result.response_record ?? null }
|
|
4882
|
+
});
|
|
4883
|
+
if (observed.hint) result.hints = [...result.hints ?? [], observed.hint];
|
|
4884
|
+
if (!observed.passed && !rateSkip) result.passed = false;
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
return findings;
|
|
4888
|
+
}
|
|
4889
|
+
function productPageStepResult(step, phaseId, context, page, schemaVersion) {
|
|
4890
|
+
const failedSchema = page.schemaFailure;
|
|
4891
|
+
return {
|
|
4892
|
+
step_id: step.id,
|
|
4893
|
+
phase_id: phaseId,
|
|
4894
|
+
task: "get_products",
|
|
4895
|
+
title: step.title,
|
|
4896
|
+
passed: page.schema_validated && page.result?.success === true,
|
|
4897
|
+
duration_ms: page.response?.duration_ms ?? 0,
|
|
4898
|
+
request: page.request,
|
|
4899
|
+
...page.response && { response_record: page.response },
|
|
4900
|
+
response: (0, import_redact_secrets.redactSecrets)(page.result?.data),
|
|
4901
|
+
extraction: page.extraction,
|
|
4902
|
+
context,
|
|
4903
|
+
validations: failedSchema ? [
|
|
4904
|
+
{
|
|
4905
|
+
check: "response_schema",
|
|
4906
|
+
passed: false,
|
|
4907
|
+
description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
|
|
4908
|
+
error: failedSchema.detail,
|
|
4909
|
+
json_pointer: null,
|
|
4910
|
+
expected: failedSchema.schema_id,
|
|
4911
|
+
actual: "schema_validation_failed",
|
|
4912
|
+
schema_id: failedSchema.schema_id,
|
|
4913
|
+
schema_url: failedSchema.schema_url,
|
|
4914
|
+
request: page.request,
|
|
4915
|
+
response: page.response,
|
|
4916
|
+
strict: { valid: false, variant: "sync" }
|
|
4917
|
+
}
|
|
4918
|
+
] : page.schema_validated ? [
|
|
4919
|
+
{
|
|
4920
|
+
check: "response_schema",
|
|
4921
|
+
passed: true,
|
|
4922
|
+
description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
|
|
4923
|
+
schema_id: `/schemas/${schemaVersion}/media-buy/get-products-response.json`,
|
|
4924
|
+
schema_url: `https://adcontextprotocol.org/schemas/${schemaVersion}/media-buy/get-products-response.json`,
|
|
4925
|
+
...page.strict_schema_validated && { strict: { valid: true, variant: "sync" } }
|
|
4926
|
+
}
|
|
4927
|
+
] : []
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4869
4930
|
function unresolvedStepContextVars(step, request, context, runState) {
|
|
4870
4931
|
const unavailableInputs = (step.context_inputs ?? []).filter(
|
|
4871
4932
|
(input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
|
|
@@ -5318,6 +5379,34 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5318
5379
|
};
|
|
5319
5380
|
}
|
|
5320
5381
|
const useRawProbe = rawProbeHeaders !== void 0 && !useSdkAuthOverride;
|
|
5382
|
+
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))) {
|
|
5383
|
+
const detail = "collect_product_pages requires fresh unkeyed reads and an unconditional wholesale first page through the official SDK client";
|
|
5384
|
+
return {
|
|
5385
|
+
step_id: step.id,
|
|
5386
|
+
phase_id: phaseId,
|
|
5387
|
+
task: step.task,
|
|
5388
|
+
title: step.title,
|
|
5389
|
+
passed: false,
|
|
5390
|
+
duration_ms: 0,
|
|
5391
|
+
context,
|
|
5392
|
+
extraction: { path: "none" },
|
|
5393
|
+
error: detail,
|
|
5394
|
+
validations: [
|
|
5395
|
+
{
|
|
5396
|
+
check: "product_page_collection",
|
|
5397
|
+
passed: false,
|
|
5398
|
+
description: detail,
|
|
5399
|
+
json_pointer: null,
|
|
5400
|
+
expected: "unkeyed unconditional wholesale query",
|
|
5401
|
+
actual: (0, import_redact_secrets.redactSecrets)(request),
|
|
5402
|
+
schema_id: null,
|
|
5403
|
+
schema_url: null,
|
|
5404
|
+
request: null,
|
|
5405
|
+
response: null
|
|
5406
|
+
}
|
|
5407
|
+
]
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5321
5410
|
const stepClient = () => authOverrideClient ?? client;
|
|
5322
5411
|
let taskResult;
|
|
5323
5412
|
let stepResult;
|
|
@@ -5327,6 +5416,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5327
5416
|
let requestUrl;
|
|
5328
5417
|
let a2aEnvelope;
|
|
5329
5418
|
let crossResponses;
|
|
5419
|
+
let productPageCollection;
|
|
5420
|
+
let collectedPageValidations = [];
|
|
5421
|
+
let actualCollectedResponse;
|
|
5422
|
+
let collectedRequestRecord;
|
|
5330
5423
|
if (step.parallel_dispatch && !useRawProbe) {
|
|
5331
5424
|
const contractsInScope = new Set(options.contracts ?? []);
|
|
5332
5425
|
if (!contractsInScope.has(import_parallel_dispatch.PARALLEL_DISPATCH_CONTRACT)) {
|
|
@@ -5495,7 +5588,96 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5495
5588
|
} else {
|
|
5496
5589
|
const captureA2a = options.protocol === "a2a";
|
|
5497
5590
|
let a2aCaptures;
|
|
5498
|
-
if (step.
|
|
5591
|
+
if (step.collect_product_pages) {
|
|
5592
|
+
const started = Date.now();
|
|
5593
|
+
const pages = [];
|
|
5594
|
+
const pageValidations = [];
|
|
5595
|
+
let pagesPassed = true;
|
|
5596
|
+
const collection = await (0, import_seeding.collectWholesaleProductPages)(stepClient(), options, request, {
|
|
5597
|
+
retainRestartEvidence: true,
|
|
5598
|
+
dispatch: (pageRequest) => (0, import_task_map.executeStoryboardTask)(stepClient(), effectiveStep.task, pageRequest, {
|
|
5599
|
+
skipIdempotencyAutoInject: testsIdempotencyKeyOmission,
|
|
5600
|
+
skipAccountValidation: testsMissingAccount,
|
|
5601
|
+
skipRequestValidation: testsSchemaInvalidRequest,
|
|
5602
|
+
preserveGovernedPayload: governedRequest,
|
|
5603
|
+
responseProjection,
|
|
5604
|
+
mediaBuyLifecycleCompatibility: options.mediaBuyLifecycleCompatibility,
|
|
5605
|
+
signal: options.signal
|
|
5606
|
+
}),
|
|
5607
|
+
onPage: async (observation) => {
|
|
5608
|
+
const physical = productPageStepResult(
|
|
5609
|
+
step,
|
|
5610
|
+
phaseId,
|
|
5611
|
+
context,
|
|
5612
|
+
observation,
|
|
5613
|
+
options._serverAdcpVersion ?? options.adcpVersion ?? import_version.ADCP_VERSION
|
|
5614
|
+
);
|
|
5615
|
+
await runState.onCollectedProductPage?.(physical, observation.page_index);
|
|
5616
|
+
pagesPassed &&= physical.passed;
|
|
5617
|
+
pageValidations.push(...physical.validations);
|
|
5618
|
+
const page = {
|
|
5619
|
+
page_index: observation.page_index,
|
|
5620
|
+
restart_count: observation.restart_count,
|
|
5621
|
+
request: observation.request,
|
|
5622
|
+
response: observation.response,
|
|
5623
|
+
extraction: observation.extraction,
|
|
5624
|
+
schema_validated: observation.schema_validated
|
|
5625
|
+
};
|
|
5626
|
+
if (pages.length >= 16 && page.response) {
|
|
5627
|
+
page.response = {
|
|
5628
|
+
...page.response,
|
|
5629
|
+
payload: {
|
|
5630
|
+
response_not_retained: "Physical page was checked; response evidence exceeded the 16-page retention budget."
|
|
5631
|
+
}
|
|
5632
|
+
};
|
|
5633
|
+
}
|
|
5634
|
+
pages.push(page);
|
|
5635
|
+
}
|
|
5636
|
+
});
|
|
5637
|
+
if (options.signal?.aborted) options.signal.throwIfAborted();
|
|
5638
|
+
const logical = collection.ok ? {
|
|
5639
|
+
...collection.catalog.firstPage.result.data,
|
|
5640
|
+
products: collection.catalog.products,
|
|
5641
|
+
...collection.catalog.errors.length > 0 && { errors: collection.catalog.errors }
|
|
5642
|
+
} : void 0;
|
|
5643
|
+
if (logical) delete logical.pagination;
|
|
5644
|
+
productPageCollection = {
|
|
5645
|
+
complete: collection.ok,
|
|
5646
|
+
page_count: pages.length,
|
|
5647
|
+
restart_count: pages.reduce((maximum, page) => Math.max(maximum, page.restart_count), 0),
|
|
5648
|
+
pages,
|
|
5649
|
+
...logical && { logical_response: (0, import_redact_secrets.redactSecrets)(logical) }
|
|
5650
|
+
};
|
|
5651
|
+
collectedRequestRecord = collection.ok ? collection.catalog.firstPage.request : collection.exchange?.request;
|
|
5652
|
+
const observedResponse = collection.ok ? collection.catalog.firstPage.response : collection.exchange?.response;
|
|
5653
|
+
responseRecord = observedResponse ?? void 0;
|
|
5654
|
+
actualCollectedResponse = collection.ok ? collection.catalog.firstPage.result.data : collection.exchange?.response?.payload;
|
|
5655
|
+
taskResult = collection.ok ? { ...collection.catalog.firstPage.result, data: logical } : {
|
|
5656
|
+
success: false,
|
|
5657
|
+
error: collection.error.replace(/^fixture discovery/, "Product page collection")
|
|
5658
|
+
};
|
|
5659
|
+
stepResult = {
|
|
5660
|
+
duration_ms: Date.now() - started,
|
|
5661
|
+
passed: collection.ok && pagesPassed,
|
|
5662
|
+
...!collection.ok && { error: collection.error.replace(/^fixture discovery/, "Product page collection") }
|
|
5663
|
+
};
|
|
5664
|
+
collectedPageValidations = pageValidations;
|
|
5665
|
+
if (!collection.ok && !pageValidations.some((validation) => (0, import_validations.validationFailsStep)(validation))) {
|
|
5666
|
+
collectedPageValidations.push({
|
|
5667
|
+
check: "product_page_collection",
|
|
5668
|
+
passed: false,
|
|
5669
|
+
description: "Wholesale collection completes one stable, duplicate-free feed",
|
|
5670
|
+
error: collection.error,
|
|
5671
|
+
json_pointer: null,
|
|
5672
|
+
expected: "a complete stable wholesale collection",
|
|
5673
|
+
actual: collection.error,
|
|
5674
|
+
schema_id: null,
|
|
5675
|
+
schema_url: null,
|
|
5676
|
+
...collection.exchange?.request && { request: collection.exchange.request },
|
|
5677
|
+
response: collection.exchange?.response ?? null
|
|
5678
|
+
});
|
|
5679
|
+
}
|
|
5680
|
+
} else if (step.parallel_dispatch) {
|
|
5499
5681
|
const started = Date.now();
|
|
5500
5682
|
crossResponses = await (0, import_parallel_dispatch.runParallelDispatches)(stepClient(), effectiveStep.task, request, {
|
|
5501
5683
|
spec: step.parallel_dispatch,
|
|
@@ -5583,7 +5765,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5583
5765
|
const requestRecord = {
|
|
5584
5766
|
transport: options.protocol === "a2a" ? "a2a" : "mcp",
|
|
5585
5767
|
operation: effectiveStep.task,
|
|
5586
|
-
payload: (0, import_redact_secrets.redactSecrets)(request),
|
|
5768
|
+
payload: collectedRequestRecord?.payload ?? (0, import_redact_secrets.redactSecrets)(request),
|
|
5587
5769
|
...effectiveRequestUrl ? { url: (0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(effectiveRequestUrl) } : {}
|
|
5588
5770
|
};
|
|
5589
5771
|
const inputSchemaStripNotices = collectInputSchemaFieldStripNotices(
|
|
@@ -5771,9 +5953,9 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5771
5953
|
success: true,
|
|
5772
5954
|
data: schemaValidationError.data
|
|
5773
5955
|
} : void 0);
|
|
5774
|
-
let validations = [];
|
|
5956
|
+
let validations = [...collectedPageValidations];
|
|
5775
5957
|
if (step.validations?.length && (validationTaskResult || httpResult)) {
|
|
5776
|
-
const resolvedValidations = step.validations.map((v) => {
|
|
5958
|
+
const resolvedValidations = step.validations.filter((v) => !productPageCollection || v.check !== "response_schema").map((v) => {
|
|
5777
5959
|
const resolved = { ...v };
|
|
5778
5960
|
if (resolved.value !== void 0) {
|
|
5779
5961
|
resolved.value = (0, import_context.injectContext)({ __v: resolved.value }, context, runState.runnerVars).__v;
|
|
@@ -5830,9 +6012,11 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
5830
6012
|
};
|
|
5831
6013
|
})()
|
|
5832
6014
|
};
|
|
5833
|
-
validations
|
|
5834
|
-
|
|
5835
|
-
|
|
6015
|
+
validations.push(
|
|
6016
|
+
...(0, import_validations.runValidations)(
|
|
6017
|
+
resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
|
|
6018
|
+
vctx
|
|
6019
|
+
)
|
|
5836
6020
|
);
|
|
5837
6021
|
for (const validation of resolvedValidations.filter((validation2) => validation2.check === "property_list_reference")) {
|
|
5838
6022
|
const result = effectiveStep.task !== "create_property_list" || !validationTaskResult?.success ? {
|
|
@@ -6102,6 +6286,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6102
6286
|
}
|
|
6103
6287
|
}
|
|
6104
6288
|
}
|
|
6289
|
+
if (productPageCollection) {
|
|
6290
|
+
for (const validation of validations) {
|
|
6291
|
+
if (collectedPageValidations.includes(validation)) continue;
|
|
6292
|
+
if (validation.json_pointer)
|
|
6293
|
+
validation.json_pointer = `/product_page_collection/logical_response${validation.json_pointer}`;
|
|
6294
|
+
validation.description = `[logical product collection] ${validation.description ?? validation.check}`;
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6105
6297
|
if (protectedViolations.length > 0) validations.push(...protectedViolations);
|
|
6106
6298
|
const allValidationsPassedFinal = validations.every((v) => !(0, import_validations.validationFailsStep)(v));
|
|
6107
6299
|
const stepFailed = !(passed && allValidationsPassedFinal);
|
|
@@ -6131,7 +6323,8 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6131
6323
|
// Legacy `response` field (new code reads `response_record`).
|
|
6132
6324
|
// Redact in case a downstream consumer still keys off it; the
|
|
6133
6325
|
// modern `response_record.payload` path is already redacted.
|
|
6134
|
-
response: (0, import_redact_secrets.redactSecrets)(taskResult?.data),
|
|
6326
|
+
response: (0, import_redact_secrets.redactSecrets)(productPageCollection ? actualCollectedResponse : taskResult?.data),
|
|
6327
|
+
...productPageCollection && { product_page_collection: productPageCollection },
|
|
6135
6328
|
validations,
|
|
6136
6329
|
// NOT projected here: the full runner threads this map from step to
|
|
6137
6330
|
// step, so stripping the reserved namespace would break the very
|
|
@@ -6155,7 +6348,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
|
|
|
6155
6348
|
next,
|
|
6156
6349
|
request: requestRecord,
|
|
6157
6350
|
...responseRecord && { response_record: responseRecord },
|
|
6158
|
-
extraction:
|
|
6351
|
+
extraction: productPageCollection ? {
|
|
6352
|
+
path: "none",
|
|
6353
|
+
note: "Authored checks and context use product_page_collection.logical_response; response_record remains an actual physical page."
|
|
6354
|
+
} : extractionFromTaskResult(taskResult),
|
|
6159
6355
|
...inputSchemaStripNotices.length > 0 && { notices: inputSchemaStripNotices },
|
|
6160
6356
|
...hints.length > 0 && { hints }
|
|
6161
6357
|
};
|