@riddledc/riddle-proof 0.7.89 → 0.7.91
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/README.md +23 -0
- package/dist/{chunk-CEBHV23V.js → chunk-2SGLFPFX.js} +517 -6
- package/dist/cli.cjs +548 -6
- package/dist/cli.js +32 -1
- package/dist/index.cjs +517 -6
- package/dist/index.js +1 -1
- package/dist/profile.cjs +517 -6
- package/dist/profile.d.cts +9 -1
- package/dist/profile.d.ts +9 -1
- package/dist/profile.js +1 -1
- package/dist/proof-run-core.d.cts +1 -1
- package/dist/proof-run-core.d.ts +1 -1
- package/dist/proof-run-engine.d.cts +3 -3
- package/dist/proof-run-engine.d.ts +3 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -8755,6 +8755,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
8755
8755
|
"frame_no_horizontal_overflow",
|
|
8756
8756
|
"text_visible",
|
|
8757
8757
|
"text_absent",
|
|
8758
|
+
"link_status",
|
|
8759
|
+
"artifact_link_status",
|
|
8758
8760
|
"route_inventory",
|
|
8759
8761
|
"no_horizontal_overflow",
|
|
8760
8762
|
"no_mobile_horizontal_overflow",
|
|
@@ -9419,6 +9421,29 @@ function normalizeStringList(value, label) {
|
|
|
9419
9421
|
if (!values.length) throw new Error(`${label} must contain non-empty strings.`);
|
|
9420
9422
|
return values;
|
|
9421
9423
|
}
|
|
9424
|
+
function normalizeHttpStatus(value, label) {
|
|
9425
|
+
if (value === void 0) return void 0;
|
|
9426
|
+
const status = numberValue3(value);
|
|
9427
|
+
if (status === void 0 || !Number.isInteger(status) || status < 100 || status > 599) {
|
|
9428
|
+
throw new Error(`${label} must be an HTTP status code.`);
|
|
9429
|
+
}
|
|
9430
|
+
return status;
|
|
9431
|
+
}
|
|
9432
|
+
function normalizeHttpStatuses(value, label) {
|
|
9433
|
+
if (value === void 0) return void 0;
|
|
9434
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
9435
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
9436
|
+
const statuses = value.map((item, index) => normalizeHttpStatus(item, `${label}[${index}]`));
|
|
9437
|
+
return Array.from(new Set(statuses));
|
|
9438
|
+
}
|
|
9439
|
+
function normalizePositiveInteger(value, label, max) {
|
|
9440
|
+
if (value === void 0) return void 0;
|
|
9441
|
+
const number = numberValue3(value);
|
|
9442
|
+
if (number === void 0 || !Number.isInteger(number) || number < 1 || max !== void 0 && number > max) {
|
|
9443
|
+
throw new Error(`${label} must be an integer from 1 to ${max ?? "unbounded"}.`);
|
|
9444
|
+
}
|
|
9445
|
+
return number;
|
|
9446
|
+
}
|
|
9422
9447
|
function validateRegexPatterns(patterns, label) {
|
|
9423
9448
|
for (const pattern of patterns || []) {
|
|
9424
9449
|
try {
|
|
@@ -9462,7 +9487,8 @@ function normalizeCheck(input, index) {
|
|
|
9462
9487
|
if (type === "url_search_param_equals" && expectedValue === void 0) {
|
|
9463
9488
|
throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
|
|
9464
9489
|
}
|
|
9465
|
-
|
|
9490
|
+
const minCount = numberValue3(input.min_count) ?? numberValue3(input.minCount);
|
|
9491
|
+
if (type === "selector_count_at_least" && minCount === void 0) {
|
|
9466
9492
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
9467
9493
|
}
|
|
9468
9494
|
const expectedCount = numberValue3(input.expected_count) ?? numberValue3(input.expectedCount) ?? numberValue3(input.count);
|
|
@@ -9478,6 +9504,21 @@ function normalizeCheck(input, index) {
|
|
|
9478
9504
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
9479
9505
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
9480
9506
|
}
|
|
9507
|
+
const isLinkStatusCheck = type === "link_status" || type === "artifact_link_status";
|
|
9508
|
+
const expectedStatus = isLinkStatusCheck ? normalizeHttpStatus(input.expected_status ?? input.expectedStatus ?? input.status, `checks[${index}] expected_status`) : void 0;
|
|
9509
|
+
const allowedStatuses = isLinkStatusCheck ? normalizeHttpStatuses(
|
|
9510
|
+
input.allowed_statuses ?? input.allowedStatuses ?? input.expected_statuses ?? input.expectedStatuses,
|
|
9511
|
+
`checks[${index}] allowed_statuses`
|
|
9512
|
+
) : void 0;
|
|
9513
|
+
const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
|
|
9514
|
+
if (isLinkStatusCheck) {
|
|
9515
|
+
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
9516
|
+
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
9517
|
+
}
|
|
9518
|
+
if (expectedCount !== void 0 && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
|
|
9519
|
+
throw new Error(`checks[${index}] ${type} expected_count must be a non-negative integer.`);
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9481
9522
|
return {
|
|
9482
9523
|
type,
|
|
9483
9524
|
label: stringValue5(input.label),
|
|
@@ -9503,8 +9544,15 @@ function normalizeCheck(input, index) {
|
|
|
9503
9544
|
allowed_console_patterns: normalizeStringList(input.allowed_console_patterns ?? input.allowedConsolePatterns ?? input.allow_console_patterns ?? input.allowConsolePatterns, `checks[${index}] allowed_console_patterns`),
|
|
9504
9545
|
allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
|
|
9505
9546
|
allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
|
|
9506
|
-
min_count:
|
|
9547
|
+
min_count: minCount,
|
|
9507
9548
|
expected_count: expectedCount,
|
|
9549
|
+
expected_status: expectedStatus,
|
|
9550
|
+
allowed_statuses: allowedStatuses,
|
|
9551
|
+
max_links: maxLinks,
|
|
9552
|
+
same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
|
|
9553
|
+
dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
|
|
9554
|
+
require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
|
|
9555
|
+
allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
|
|
9508
9556
|
max_overflow_px: numberValue3(input.max_overflow_px),
|
|
9509
9557
|
timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
|
|
9510
9558
|
run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
|
|
@@ -9597,6 +9645,97 @@ function checkLabel(check) {
|
|
|
9597
9645
|
function selectorKey(check) {
|
|
9598
9646
|
return check.selector || "";
|
|
9599
9647
|
}
|
|
9648
|
+
function linkStatusSelector(check) {
|
|
9649
|
+
return check.selector || check.link_selector || "a[href]";
|
|
9650
|
+
}
|
|
9651
|
+
function linkStatusAllowedStatuses(check) {
|
|
9652
|
+
if (check.allowed_statuses?.length) return check.allowed_statuses;
|
|
9653
|
+
if (check.expected_status !== void 0) return [check.expected_status];
|
|
9654
|
+
return void 0;
|
|
9655
|
+
}
|
|
9656
|
+
function linkStatusIsAllowed(status, check) {
|
|
9657
|
+
if (status === void 0) return false;
|
|
9658
|
+
const allowed = linkStatusAllowedStatuses(check);
|
|
9659
|
+
return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
9660
|
+
}
|
|
9661
|
+
function linkStatusResultOk(result, check) {
|
|
9662
|
+
const status = numberValue3(result.status);
|
|
9663
|
+
if (!linkStatusIsAllowed(status, check)) return false;
|
|
9664
|
+
if (stringValue5(result.error)) return false;
|
|
9665
|
+
if (result.ok === false) return false;
|
|
9666
|
+
if (check.require_nonzero_bytes) {
|
|
9667
|
+
const bytes = numberValue3(result.bytes);
|
|
9668
|
+
const contentLength = numberValue3(result.content_length);
|
|
9669
|
+
return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
|
|
9670
|
+
}
|
|
9671
|
+
return true;
|
|
9672
|
+
}
|
|
9673
|
+
function linkStatusEvidenceForCheck(viewport, check) {
|
|
9674
|
+
const evidence = viewport.link_statuses?.[linkStatusSelector(check)];
|
|
9675
|
+
return isRecord2(evidence) ? evidence : void 0;
|
|
9676
|
+
}
|
|
9677
|
+
function summarizeLinkStatusEvidence(viewport, check) {
|
|
9678
|
+
const linkEvidence = linkStatusEvidenceForCheck(viewport, check);
|
|
9679
|
+
if (!linkEvidence) {
|
|
9680
|
+
return {
|
|
9681
|
+
viewport: viewport.name,
|
|
9682
|
+
selector: linkStatusSelector(check),
|
|
9683
|
+
total_count: 0,
|
|
9684
|
+
ok_count: 0,
|
|
9685
|
+
failed_count: 1,
|
|
9686
|
+
failures: [{ code: "link_status_evidence_missing" }]
|
|
9687
|
+
};
|
|
9688
|
+
}
|
|
9689
|
+
const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord2) : [];
|
|
9690
|
+
const totalCount = numberValue3(linkEvidence.total_count) ?? results.length;
|
|
9691
|
+
const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
|
|
9692
|
+
const failures = results.filter((result) => !linkStatusResultOk(result, check)).map((result) => ({
|
|
9693
|
+
code: "link_status_failed",
|
|
9694
|
+
url: stringValue5(result.url) ?? null,
|
|
9695
|
+
status: numberValue3(result.status) ?? null,
|
|
9696
|
+
method: stringValue5(result.method) ?? null,
|
|
9697
|
+
error: stringValue5(result.error) ?? null,
|
|
9698
|
+
bytes: numberValue3(result.bytes) ?? numberValue3(result.content_length) ?? null
|
|
9699
|
+
}));
|
|
9700
|
+
if (stringValue5(linkEvidence.error)) {
|
|
9701
|
+
failures.push({ code: "link_status_capture_failed", error: stringValue5(linkEvidence.error) ?? "" });
|
|
9702
|
+
}
|
|
9703
|
+
const recordedFailedCount = numberValue3(linkEvidence.failed_count) ?? 0;
|
|
9704
|
+
if (!failures.length && recordedFailedCount > 0) {
|
|
9705
|
+
const recordedFailures = Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [];
|
|
9706
|
+
failures.push({
|
|
9707
|
+
code: "link_status_recorded_failures",
|
|
9708
|
+
failed_count: recordedFailedCount,
|
|
9709
|
+
failures: toJsonValue(recordedFailures)
|
|
9710
|
+
});
|
|
9711
|
+
}
|
|
9712
|
+
if (linkEvidence.truncated === true) {
|
|
9713
|
+
failures.push({
|
|
9714
|
+
code: "link_status_probe_truncated",
|
|
9715
|
+
discovered_count: numberValue3(linkEvidence.discovered_count) ?? totalCount,
|
|
9716
|
+
max_links: numberValue3(linkEvidence.max_links) ?? check.max_links ?? 100
|
|
9717
|
+
});
|
|
9718
|
+
}
|
|
9719
|
+
if (check.expected_count !== void 0 && totalCount !== check.expected_count) {
|
|
9720
|
+
failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
|
|
9721
|
+
}
|
|
9722
|
+
if (check.min_count !== void 0 && totalCount < check.min_count) {
|
|
9723
|
+
failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
|
|
9724
|
+
}
|
|
9725
|
+
const statusCounts = isRecord2(linkEvidence.status_counts) ? linkEvidence.status_counts : {};
|
|
9726
|
+
return {
|
|
9727
|
+
viewport: viewport.name,
|
|
9728
|
+
selector: linkStatusSelector(check),
|
|
9729
|
+
total_count: totalCount,
|
|
9730
|
+
discovered_count: numberValue3(linkEvidence.discovered_count) ?? totalCount,
|
|
9731
|
+
ok_count: numberValue3(linkEvidence.ok_count) ?? okCount,
|
|
9732
|
+
failed_count: failures.length,
|
|
9733
|
+
truncated: linkEvidence.truncated === true,
|
|
9734
|
+
max_links: numberValue3(linkEvidence.max_links) ?? check.max_links ?? 100,
|
|
9735
|
+
status_counts: statusCounts,
|
|
9736
|
+
failures: failures.slice(0, 20)
|
|
9737
|
+
};
|
|
9738
|
+
}
|
|
9600
9739
|
function textKey(check) {
|
|
9601
9740
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
9602
9741
|
}
|
|
@@ -10076,6 +10215,26 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
10076
10215
|
message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
|
|
10077
10216
|
};
|
|
10078
10217
|
}
|
|
10218
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
10219
|
+
const selector = linkStatusSelector(check);
|
|
10220
|
+
const summaries = viewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
10221
|
+
const failed = summaries.filter((summary) => (numberValue3(summary.failed_count) ?? 0) > 0);
|
|
10222
|
+
return {
|
|
10223
|
+
type: check.type,
|
|
10224
|
+
label: checkLabel(check),
|
|
10225
|
+
status: failed.length ? "failed" : "passed",
|
|
10226
|
+
evidence: {
|
|
10227
|
+
selector,
|
|
10228
|
+
expected_count: check.expected_count ?? null,
|
|
10229
|
+
min_count: check.min_count ?? null,
|
|
10230
|
+
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
10231
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
10232
|
+
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
10233
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue5(summary.viewport) ?? null, failure })) : [])
|
|
10234
|
+
},
|
|
10235
|
+
message: failed.length ? `Link status failed in ${failed.length} viewport(s).` : void 0
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10079
10238
|
if (check.type === "route_inventory") {
|
|
10080
10239
|
const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord2(item.inventory));
|
|
10081
10240
|
if (!inventories.length) {
|
|
@@ -10658,6 +10817,98 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
10658
10817
|
}
|
|
10659
10818
|
return { matched: true, positions };
|
|
10660
10819
|
}
|
|
10820
|
+
function linkStatusSelector(check) {
|
|
10821
|
+
return check.selector || check.link_selector || "a[href]";
|
|
10822
|
+
}
|
|
10823
|
+
function linkStatusAllowedStatuses(check) {
|
|
10824
|
+
if (Array.isArray(check.allowed_statuses) && check.allowed_statuses.length) return check.allowed_statuses;
|
|
10825
|
+
if (typeof check.expected_status === "number" && Number.isFinite(check.expected_status)) return [check.expected_status];
|
|
10826
|
+
return undefined;
|
|
10827
|
+
}
|
|
10828
|
+
function linkStatusIsAllowed(status, check) {
|
|
10829
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
10830
|
+
const allowed = linkStatusAllowedStatuses(check);
|
|
10831
|
+
return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
10832
|
+
}
|
|
10833
|
+
function linkStatusResultOk(result, check) {
|
|
10834
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
10835
|
+
if (!linkStatusIsAllowed(result.status, check)) return false;
|
|
10836
|
+
if (typeof result.error === "string" && result.error.trim()) return false;
|
|
10837
|
+
if (result.ok === false) return false;
|
|
10838
|
+
if (check.require_nonzero_bytes === true) {
|
|
10839
|
+
const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
|
|
10840
|
+
const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
|
|
10841
|
+
return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
|
|
10842
|
+
}
|
|
10843
|
+
return true;
|
|
10844
|
+
}
|
|
10845
|
+
function summarizeLinkStatusEvidence(viewport, check) {
|
|
10846
|
+
const selector = linkStatusSelector(check);
|
|
10847
|
+
const linkEvidence = viewport && viewport.link_statuses && viewport.link_statuses[selector];
|
|
10848
|
+
if (!linkEvidence || typeof linkEvidence !== "object" || Array.isArray(linkEvidence)) {
|
|
10849
|
+
return {
|
|
10850
|
+
viewport: viewport && viewport.name,
|
|
10851
|
+
selector,
|
|
10852
|
+
total_count: 0,
|
|
10853
|
+
ok_count: 0,
|
|
10854
|
+
failed_count: 1,
|
|
10855
|
+
failures: [{ code: "link_status_evidence_missing" }],
|
|
10856
|
+
};
|
|
10857
|
+
}
|
|
10858
|
+
const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter((result) => result && typeof result === "object" && !Array.isArray(result)) : [];
|
|
10859
|
+
const totalCount = typeof linkEvidence.total_count === "number" && Number.isFinite(linkEvidence.total_count) ? linkEvidence.total_count : results.length;
|
|
10860
|
+
const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
|
|
10861
|
+
const failures = results
|
|
10862
|
+
.filter((result) => !linkStatusResultOk(result, check))
|
|
10863
|
+
.map((result) => ({
|
|
10864
|
+
code: "link_status_failed",
|
|
10865
|
+
url: typeof result.url === "string" ? result.url : null,
|
|
10866
|
+
status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
|
|
10867
|
+
method: typeof result.method === "string" ? result.method : null,
|
|
10868
|
+
error: typeof result.error === "string" ? result.error : null,
|
|
10869
|
+
bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
|
|
10870
|
+
? result.bytes
|
|
10871
|
+
: typeof result.content_length === "number" && Number.isFinite(result.content_length)
|
|
10872
|
+
? result.content_length
|
|
10873
|
+
: null,
|
|
10874
|
+
}));
|
|
10875
|
+
if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
|
|
10876
|
+
failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
|
|
10877
|
+
}
|
|
10878
|
+
const recordedFailedCount = typeof linkEvidence.failed_count === "number" && Number.isFinite(linkEvidence.failed_count) ? linkEvidence.failed_count : 0;
|
|
10879
|
+
if (!failures.length && recordedFailedCount > 0) {
|
|
10880
|
+
failures.push({
|
|
10881
|
+
code: "link_status_recorded_failures",
|
|
10882
|
+
failed_count: recordedFailedCount,
|
|
10883
|
+
failures: Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [],
|
|
10884
|
+
});
|
|
10885
|
+
}
|
|
10886
|
+
if (linkEvidence.truncated === true) {
|
|
10887
|
+
failures.push({
|
|
10888
|
+
code: "link_status_probe_truncated",
|
|
10889
|
+
discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
|
|
10890
|
+
max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
|
|
10891
|
+
});
|
|
10892
|
+
}
|
|
10893
|
+
if (typeof check.expected_count === "number" && Number.isFinite(check.expected_count) && totalCount !== check.expected_count) {
|
|
10894
|
+
failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
|
|
10895
|
+
}
|
|
10896
|
+
if (typeof check.min_count === "number" && Number.isFinite(check.min_count) && totalCount < check.min_count) {
|
|
10897
|
+
failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
|
|
10898
|
+
}
|
|
10899
|
+
return {
|
|
10900
|
+
viewport: viewport && viewport.name,
|
|
10901
|
+
selector,
|
|
10902
|
+
total_count: totalCount,
|
|
10903
|
+
discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
|
|
10904
|
+
ok_count: typeof linkEvidence.ok_count === "number" ? linkEvidence.ok_count : okCount,
|
|
10905
|
+
failed_count: failures.length,
|
|
10906
|
+
truncated: linkEvidence.truncated === true,
|
|
10907
|
+
max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
|
|
10908
|
+
status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
|
|
10909
|
+
failures: failures.slice(0, 20),
|
|
10910
|
+
};
|
|
10911
|
+
}
|
|
10661
10912
|
function frameEvidenceForSelector(viewport, selector) {
|
|
10662
10913
|
const container = viewport.frames && viewport.frames[selector || ""];
|
|
10663
10914
|
if (!container || typeof container !== "object" || Array.isArray(container)) return [];
|
|
@@ -11311,6 +11562,29 @@ function assessProfile(profile, evidence) {
|
|
|
11311
11562
|
});
|
|
11312
11563
|
continue;
|
|
11313
11564
|
}
|
|
11565
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
11566
|
+
const selector = linkStatusSelector(check);
|
|
11567
|
+
const summaries = checkViewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
11568
|
+
const failed = summaries.filter((summary) => summary.failed_count > 0);
|
|
11569
|
+
checks.push({
|
|
11570
|
+
type: check.type,
|
|
11571
|
+
label: check.label || check.type,
|
|
11572
|
+
status: failed.length ? "failed" : "passed",
|
|
11573
|
+
evidence: {
|
|
11574
|
+
selector,
|
|
11575
|
+
expected_count: check.expected_count ?? null,
|
|
11576
|
+
min_count: check.min_count ?? null,
|
|
11577
|
+
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
11578
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
11579
|
+
viewports: summaries,
|
|
11580
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures)
|
|
11581
|
+
? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
|
|
11582
|
+
: []),
|
|
11583
|
+
},
|
|
11584
|
+
message: failed.length ? "Link status failed in " + failed.length + " viewport(s)." : undefined,
|
|
11585
|
+
});
|
|
11586
|
+
continue;
|
|
11587
|
+
}
|
|
11314
11588
|
if (check.type === "route_inventory") {
|
|
11315
11589
|
const inventories = checkViewports
|
|
11316
11590
|
.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
|
|
@@ -11551,6 +11825,10 @@ function textMatches(sample, check) {
|
|
|
11551
11825
|
}
|
|
11552
11826
|
return String(sample || "").includes(check.text || "");
|
|
11553
11827
|
}
|
|
11828
|
+
function profileCheckAppliesToViewport(check, viewport) {
|
|
11829
|
+
if (!Array.isArray(check.viewports) || !check.viewports.length) return true;
|
|
11830
|
+
return Boolean(viewport && viewport.name && check.viewports.includes(viewport.name));
|
|
11831
|
+
}
|
|
11554
11832
|
function setupActionType(action) {
|
|
11555
11833
|
return String(action && action.type ? action.type : "").replace(/-/g, "_");
|
|
11556
11834
|
}
|
|
@@ -11568,6 +11846,30 @@ function setupTextMatches(sample, action) {
|
|
|
11568
11846
|
}
|
|
11569
11847
|
return String(sample || "").includes(action.text || "");
|
|
11570
11848
|
}
|
|
11849
|
+
async function waitForAnyVisibleSelector(context, selector, timeout) {
|
|
11850
|
+
const deadline = Date.now() + setupNumber(timeout, 15000);
|
|
11851
|
+
let lastReason = "selector_not_found";
|
|
11852
|
+
while (Date.now() <= deadline) {
|
|
11853
|
+
try {
|
|
11854
|
+
const locator = context.locator(selector);
|
|
11855
|
+
const count = await locator.count();
|
|
11856
|
+
if (!count) {
|
|
11857
|
+
lastReason = "selector_not_found";
|
|
11858
|
+
} else {
|
|
11859
|
+
lastReason = "no_visible_match";
|
|
11860
|
+
for (let index = 0; index < count; index += 1) {
|
|
11861
|
+
if (await locator.nth(index).isVisible().catch(() => false)) {
|
|
11862
|
+
return { ok: true, count, index };
|
|
11863
|
+
}
|
|
11864
|
+
}
|
|
11865
|
+
}
|
|
11866
|
+
} catch (error) {
|
|
11867
|
+
lastReason = String(error && error.message ? error.message : error).slice(0, 500);
|
|
11868
|
+
}
|
|
11869
|
+
await page.waitForTimeout(200);
|
|
11870
|
+
}
|
|
11871
|
+
throw new Error("No visible match for selector " + selector + ": " + lastReason);
|
|
11872
|
+
}
|
|
11571
11873
|
function setupHasOwn(action, key) {
|
|
11572
11874
|
return Boolean(action) && Object.keys(action).includes(key);
|
|
11573
11875
|
}
|
|
@@ -11901,7 +12203,7 @@ async function executeSetupAction(action, ordinal, viewport) {
|
|
|
11901
12203
|
if (type === "wait_for_selector") {
|
|
11902
12204
|
const scope = await setupActionScope(action, timeout);
|
|
11903
12205
|
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
11904
|
-
await scope.context
|
|
12206
|
+
await waitForAnyVisibleSelector(scope.context, action.selector, timeout);
|
|
11905
12207
|
return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
|
|
11906
12208
|
}
|
|
11907
12209
|
if (type === "drag") {
|
|
@@ -12369,6 +12671,196 @@ async function selectorTextSequence(selector) {
|
|
|
12369
12671
|
};
|
|
12370
12672
|
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
12371
12673
|
}
|
|
12674
|
+
function linkProbeMaxLinks(check) {
|
|
12675
|
+
const value = Number(check.max_links || check.maxLinks || check.limit || 100);
|
|
12676
|
+
return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
|
|
12677
|
+
}
|
|
12678
|
+
function linkProbeDedupe(check) {
|
|
12679
|
+
return check.dedupe !== false;
|
|
12680
|
+
}
|
|
12681
|
+
function linkProbeSameOriginOnly(check) {
|
|
12682
|
+
return check.same_origin_only === true;
|
|
12683
|
+
}
|
|
12684
|
+
async function collectLinkCandidates(selector) {
|
|
12685
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
12686
|
+
const compact = (value, limit) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit || 160);
|
|
12687
|
+
return elements.map((element, index) => {
|
|
12688
|
+
const tag = element && element.tagName ? element.tagName.toLowerCase() : "element";
|
|
12689
|
+
const href = element && typeof element.href === "string" ? element.href : "";
|
|
12690
|
+
const src = element && typeof element.src === "string" ? element.src : "";
|
|
12691
|
+
const currentSrc = element && typeof element.currentSrc === "string" ? element.currentSrc : "";
|
|
12692
|
+
const attrHref = element && typeof element.getAttribute === "function" ? element.getAttribute("href") || "" : "";
|
|
12693
|
+
const attrSrc = element && typeof element.getAttribute === "function" ? element.getAttribute("src") || "" : "";
|
|
12694
|
+
const attrPoster = element && typeof element.getAttribute === "function" ? element.getAttribute("poster") || "" : "";
|
|
12695
|
+
const raw = href || currentSrc || src || attrHref || attrSrc || attrPoster;
|
|
12696
|
+
if (!raw) return null;
|
|
12697
|
+
let url = "";
|
|
12698
|
+
try {
|
|
12699
|
+
url = new URL(raw, location.href).href;
|
|
12700
|
+
} catch {}
|
|
12701
|
+
if (!url) return null;
|
|
12702
|
+
return {
|
|
12703
|
+
index,
|
|
12704
|
+
tag,
|
|
12705
|
+
url,
|
|
12706
|
+
raw,
|
|
12707
|
+
text: compact(element.innerText || element.textContent || "", 160),
|
|
12708
|
+
alt: compact(element.getAttribute && element.getAttribute("alt"), 80),
|
|
12709
|
+
};
|
|
12710
|
+
}).filter(Boolean);
|
|
12711
|
+
}).catch((error) => ({ error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
12712
|
+
}
|
|
12713
|
+
function linkProbeAllowed(status, check) {
|
|
12714
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
12715
|
+
const allowed = Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
|
|
12716
|
+
? check.allowed_statuses
|
|
12717
|
+
: typeof check.expected_status === "number" && Number.isFinite(check.expected_status)
|
|
12718
|
+
? [check.expected_status]
|
|
12719
|
+
: null;
|
|
12720
|
+
return allowed ? allowed.includes(status) : status >= 200 && status < 400;
|
|
12721
|
+
}
|
|
12722
|
+
function linkProbeResponseFields(response, method) {
|
|
12723
|
+
const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
|
|
12724
|
+
const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
|
|
12725
|
+
return {
|
|
12726
|
+
method,
|
|
12727
|
+
status: response.status,
|
|
12728
|
+
redirected: Boolean(response.redirected),
|
|
12729
|
+
final_url: response.url || null,
|
|
12730
|
+
content_type: response.headers && typeof response.headers.get === "function" ? response.headers.get("content-type") : null,
|
|
12731
|
+
content_length: contentLength,
|
|
12732
|
+
};
|
|
12733
|
+
}
|
|
12734
|
+
async function probeLinkStatus(candidate, check) {
|
|
12735
|
+
const requireNonzeroBytes = check.require_nonzero_bytes === true;
|
|
12736
|
+
const allowGetFallback = check.allow_get_fallback !== false;
|
|
12737
|
+
const result = {
|
|
12738
|
+
url: candidate.url,
|
|
12739
|
+
tag: candidate.tag,
|
|
12740
|
+
text: candidate.text || null,
|
|
12741
|
+
status: null,
|
|
12742
|
+
method: null,
|
|
12743
|
+
ok: false,
|
|
12744
|
+
content_type: null,
|
|
12745
|
+
content_length: null,
|
|
12746
|
+
bytes: null,
|
|
12747
|
+
redirected: false,
|
|
12748
|
+
final_url: null,
|
|
12749
|
+
error: null,
|
|
12750
|
+
};
|
|
12751
|
+
const applyResponse = async (response, method, readBytes) => {
|
|
12752
|
+
Object.assign(result, linkProbeResponseFields(response, method));
|
|
12753
|
+
if (readBytes || requireNonzeroBytes) {
|
|
12754
|
+
try {
|
|
12755
|
+
const buffer = await response.arrayBuffer();
|
|
12756
|
+
result.bytes = buffer.byteLength;
|
|
12757
|
+
} catch (error) {
|
|
12758
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
12759
|
+
}
|
|
12760
|
+
}
|
|
12761
|
+
result.ok = linkProbeAllowed(result.status, check)
|
|
12762
|
+
&& (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
|
|
12763
|
+
&& !result.error;
|
|
12764
|
+
};
|
|
12765
|
+
try {
|
|
12766
|
+
const response = await fetch(candidate.url, { method: "HEAD", redirect: "follow", cache: "no-store" });
|
|
12767
|
+
await applyResponse(response, "HEAD", false);
|
|
12768
|
+
if (result.ok || !allowGetFallback) return result;
|
|
12769
|
+
} catch (error) {
|
|
12770
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
12771
|
+
if (!allowGetFallback) return result;
|
|
12772
|
+
}
|
|
12773
|
+
try {
|
|
12774
|
+
result.error = null;
|
|
12775
|
+
const response = await fetch(candidate.url, {
|
|
12776
|
+
method: "GET",
|
|
12777
|
+
redirect: "follow",
|
|
12778
|
+
cache: "no-store",
|
|
12779
|
+
headers: { Range: "bytes=0-0" },
|
|
12780
|
+
});
|
|
12781
|
+
await applyResponse(response, "GET", requireNonzeroBytes);
|
|
12782
|
+
return result;
|
|
12783
|
+
} catch (error) {
|
|
12784
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
12785
|
+
result.ok = false;
|
|
12786
|
+
return result;
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12789
|
+
async function collectLinkStatus(check) {
|
|
12790
|
+
const selector = linkStatusSelector(check);
|
|
12791
|
+
const candidateResult = await collectLinkCandidates(selector);
|
|
12792
|
+
if (candidateResult && candidateResult.error) {
|
|
12793
|
+
return {
|
|
12794
|
+
version: "riddle-proof.link-status.v1",
|
|
12795
|
+
selector,
|
|
12796
|
+
total_count: 0,
|
|
12797
|
+
discovered_count: 0,
|
|
12798
|
+
ok_count: 0,
|
|
12799
|
+
failed_count: 1,
|
|
12800
|
+
failures: [{ code: "link_status_capture_failed", error: candidateResult.error }],
|
|
12801
|
+
results: [],
|
|
12802
|
+
status_counts: {},
|
|
12803
|
+
error: candidateResult.error,
|
|
12804
|
+
};
|
|
12805
|
+
}
|
|
12806
|
+
const baseUrl = page.url() || targetUrl;
|
|
12807
|
+
let candidates = Array.isArray(candidateResult) ? candidateResult : [];
|
|
12808
|
+
if (linkProbeSameOriginOnly(check)) {
|
|
12809
|
+
const origin = new URL(baseUrl).origin;
|
|
12810
|
+
candidates = candidates.filter((candidate) => {
|
|
12811
|
+
try { return new URL(candidate.url).origin === origin; } catch { return false; }
|
|
12812
|
+
});
|
|
12813
|
+
}
|
|
12814
|
+
const discoveredCount = candidates.length;
|
|
12815
|
+
if (linkProbeDedupe(check)) {
|
|
12816
|
+
const seen = new Set();
|
|
12817
|
+
candidates = candidates.filter((candidate) => {
|
|
12818
|
+
if (seen.has(candidate.url)) return false;
|
|
12819
|
+
seen.add(candidate.url);
|
|
12820
|
+
return true;
|
|
12821
|
+
});
|
|
12822
|
+
}
|
|
12823
|
+
const maxLinks = linkProbeMaxLinks(check);
|
|
12824
|
+
const selected = candidates.slice(0, maxLinks);
|
|
12825
|
+
const results = [];
|
|
12826
|
+
for (const candidate of selected) {
|
|
12827
|
+
results.push(await probeLinkStatus(candidate, check));
|
|
12828
|
+
}
|
|
12829
|
+
const failures = results.filter((result) => !result.ok).map((result) => ({
|
|
12830
|
+
code: "link_status_failed",
|
|
12831
|
+
url: result.url,
|
|
12832
|
+
status: result.status,
|
|
12833
|
+
method: result.method,
|
|
12834
|
+
error: result.error,
|
|
12835
|
+
bytes: result.bytes == null ? result.content_length : result.bytes,
|
|
12836
|
+
}));
|
|
12837
|
+
const statusCounts = {};
|
|
12838
|
+
for (const result of results) {
|
|
12839
|
+
const key = result.status == null ? "error" : String(result.status);
|
|
12840
|
+
statusCounts[key] = (statusCounts[key] || 0) + 1;
|
|
12841
|
+
}
|
|
12842
|
+
return {
|
|
12843
|
+
version: "riddle-proof.link-status.v1",
|
|
12844
|
+
selector,
|
|
12845
|
+
max_links: maxLinks,
|
|
12846
|
+
same_origin_only: linkProbeSameOriginOnly(check),
|
|
12847
|
+
dedupe: linkProbeDedupe(check),
|
|
12848
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
12849
|
+
allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
|
|
12850
|
+
? check.allowed_statuses
|
|
12851
|
+
: typeof check.expected_status === "number"
|
|
12852
|
+
? [check.expected_status]
|
|
12853
|
+
: ["2xx", "3xx"],
|
|
12854
|
+
discovered_count: discoveredCount,
|
|
12855
|
+
total_count: selected.length,
|
|
12856
|
+
truncated: candidates.length > selected.length,
|
|
12857
|
+
ok_count: results.filter((result) => result.ok).length,
|
|
12858
|
+
failed_count: failures.length,
|
|
12859
|
+
status_counts: statusCounts,
|
|
12860
|
+
failures: failures.slice(0, 20),
|
|
12861
|
+
results,
|
|
12862
|
+
};
|
|
12863
|
+
}
|
|
12372
12864
|
async function frameEvidence(selector) {
|
|
12373
12865
|
const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
|
|
12374
12866
|
let handles = [];
|
|
@@ -12727,7 +13219,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
12727
13219
|
try {
|
|
12728
13220
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
|
|
12729
13221
|
if (profile.target.wait_for_selector) {
|
|
12730
|
-
await page
|
|
13222
|
+
await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000).catch(() => {});
|
|
12731
13223
|
}
|
|
12732
13224
|
const index = await findInventoryLinkIndex(check, expectedRoute.path);
|
|
12733
13225
|
if (index < 0) {
|
|
@@ -12786,7 +13278,7 @@ async function captureViewport(viewport) {
|
|
|
12786
13278
|
}
|
|
12787
13279
|
if (!navigationError && profile.target.wait_for_selector) {
|
|
12788
13280
|
try {
|
|
12789
|
-
await page
|
|
13281
|
+
await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000);
|
|
12790
13282
|
} catch (error) {
|
|
12791
13283
|
waitError = String(error && error.message ? error.message : error).slice(0, 1000);
|
|
12792
13284
|
}
|
|
@@ -12888,7 +13380,9 @@ async function captureViewport(viewport) {
|
|
|
12888
13380
|
const frames = {};
|
|
12889
13381
|
const text_sequences = {};
|
|
12890
13382
|
const text_matches = {};
|
|
13383
|
+
const link_statuses = {};
|
|
12891
13384
|
for (const check of profile.checks || []) {
|
|
13385
|
+
if (!profileCheckAppliesToViewport(check, viewport)) continue;
|
|
12892
13386
|
if (
|
|
12893
13387
|
(
|
|
12894
13388
|
check.type === "selector_visible"
|
|
@@ -12912,6 +13406,10 @@ async function captureViewport(viewport) {
|
|
|
12912
13406
|
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
12913
13407
|
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
12914
13408
|
}
|
|
13409
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
13410
|
+
const selector = linkStatusSelector(check);
|
|
13411
|
+
link_statuses[selector] = link_statuses[selector] || await collectLinkStatus(check);
|
|
13412
|
+
}
|
|
12915
13413
|
}
|
|
12916
13414
|
const screenshotLabel = profileSlug + "-" + viewport.name;
|
|
12917
13415
|
try {
|
|
@@ -12920,7 +13418,7 @@ async function captureViewport(viewport) {
|
|
|
12920
13418
|
pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
|
|
12921
13419
|
}
|
|
12922
13420
|
let routeInventory;
|
|
12923
|
-
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
|
|
13421
|
+
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory" && profileCheckAppliesToViewport(check, viewport));
|
|
12924
13422
|
const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
|
|
12925
13423
|
if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
|
|
12926
13424
|
try {
|
|
@@ -12975,6 +13473,7 @@ async function captureViewport(viewport) {
|
|
|
12975
13473
|
frames,
|
|
12976
13474
|
text_sequences,
|
|
12977
13475
|
text_matches,
|
|
13476
|
+
link_statuses,
|
|
12978
13477
|
route_inventory: routeInventory,
|
|
12979
13478
|
setup_action_results: setupActionResults,
|
|
12980
13479
|
screenshot_label: screenshotLabel,
|
|
@@ -13023,6 +13522,18 @@ function buildProfileEvidence(currentViewports) {
|
|
|
13023
13522
|
),
|
|
13024
13523
|
})),
|
|
13025
13524
|
})),
|
|
13525
|
+
link_status: currentViewports
|
|
13526
|
+
.filter((viewport) => viewport.link_statuses && Object.keys(viewport.link_statuses).length)
|
|
13527
|
+
.map((viewport) => ({
|
|
13528
|
+
viewport: viewport.name,
|
|
13529
|
+
selectors: Object.entries(viewport.link_statuses || {}).map(([selector, statusSet]) => ({
|
|
13530
|
+
selector,
|
|
13531
|
+
total_count: statusSet && statusSet.total_count,
|
|
13532
|
+
ok_count: statusSet && statusSet.ok_count,
|
|
13533
|
+
failed_count: statusSet && statusSet.failed_count,
|
|
13534
|
+
truncated: statusSet && statusSet.truncated === true,
|
|
13535
|
+
})),
|
|
13536
|
+
})),
|
|
13026
13537
|
route_inventory: currentViewports
|
|
13027
13538
|
.filter((viewport) => viewport.route_inventory)
|
|
13028
13539
|
.map((viewport) => ({
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
resolveRiddleProofProfileTimeoutSec,
|
|
59
59
|
slugifyRiddleProofProfileName,
|
|
60
60
|
summarizeRiddleProofProfileResult
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-2SGLFPFX.js";
|
|
62
62
|
import {
|
|
63
63
|
DEFAULT_RIDDLE_API_BASE_URL,
|
|
64
64
|
DEFAULT_RIDDLE_API_KEY_FILE,
|