@riddledc/riddle-proof 0.7.125 → 0.7.127

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/cli.cjs CHANGED
@@ -7076,6 +7076,9 @@ function valueFromOwn(input, ...keys) {
7076
7076
  function numberValue(value) {
7077
7077
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7078
7078
  }
7079
+ function booleanValue(value) {
7080
+ return typeof value === "boolean" ? value : void 0;
7081
+ }
7079
7082
  function horizontalBoundsOverflowPx(value) {
7080
7083
  if (!isRecord(value)) return 0;
7081
7084
  let max = maxPositiveNumber(
@@ -7170,6 +7173,156 @@ function toJsonValue(value) {
7170
7173
  if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
7171
7174
  return String(value);
7172
7175
  }
7176
+ function jsonValueType(value) {
7177
+ if (value === null) return "null";
7178
+ if (Array.isArray(value)) return "array";
7179
+ if (typeof value === "boolean") return "boolean";
7180
+ if (typeof value === "number") return "number";
7181
+ if (typeof value === "string") return "string";
7182
+ return "object";
7183
+ }
7184
+ function deepJsonEqual(left, right) {
7185
+ if (left === right) return true;
7186
+ if (typeof left !== typeof right) return false;
7187
+ if (left === null || right === null) return left === right;
7188
+ if (typeof left !== "object" || typeof right !== "object") return false;
7189
+ if (Array.isArray(left) || Array.isArray(right)) {
7190
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
7191
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
7192
+ }
7193
+ if (!isRecord(left) || !isRecord(right)) return false;
7194
+ const leftKeys = Object.keys(left).sort();
7195
+ const rightKeys = Object.keys(right).sort();
7196
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
7197
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
7198
+ }
7199
+ function jsonContains(observed, expected) {
7200
+ if (typeof observed === "string" && typeof expected === "string") {
7201
+ return observed.includes(expected);
7202
+ }
7203
+ if (Array.isArray(observed)) {
7204
+ return observed.some((item) => deepJsonEqual(item, expected));
7205
+ }
7206
+ if (isRecord(observed) && isRecord(expected)) {
7207
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
7208
+ }
7209
+ return false;
7210
+ }
7211
+ function parseJsonPathSegments(path7) {
7212
+ let input = path7.trim();
7213
+ if (!input) throw new Error("path is empty");
7214
+ if (input === "$") return [];
7215
+ if (input.startsWith("$.")) input = input.slice(2);
7216
+ else if (input.startsWith("$[")) input = input.slice(1);
7217
+ const segments = [];
7218
+ let token = "";
7219
+ const pushToken = () => {
7220
+ if (!token) return;
7221
+ segments.push(token);
7222
+ token = "";
7223
+ };
7224
+ for (let index = 0; index < input.length; index += 1) {
7225
+ const char = input[index];
7226
+ if (char === ".") {
7227
+ pushToken();
7228
+ continue;
7229
+ }
7230
+ if (char !== "[") {
7231
+ token += char;
7232
+ continue;
7233
+ }
7234
+ pushToken();
7235
+ const closeIndex = input.indexOf("]", index + 1);
7236
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
7237
+ const bracket = input.slice(index + 1, closeIndex).trim();
7238
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
7239
+ if (/^\d+$/.test(bracket)) {
7240
+ segments.push(Number(bracket));
7241
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
7242
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
7243
+ segments.push(String(JSON.parse(quoted)));
7244
+ } else {
7245
+ segments.push(bracket);
7246
+ }
7247
+ index = closeIndex;
7248
+ }
7249
+ pushToken();
7250
+ return segments;
7251
+ }
7252
+ function resolveJsonPath(root, path7) {
7253
+ let segments;
7254
+ try {
7255
+ segments = parseJsonPathSegments(path7);
7256
+ } catch (error) {
7257
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
7258
+ }
7259
+ let current = root;
7260
+ for (const segment of segments) {
7261
+ if (typeof segment === "number") {
7262
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
7263
+ current = current[segment];
7264
+ continue;
7265
+ }
7266
+ if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
7267
+ current = current[segment];
7268
+ }
7269
+ return { exists: true, value: current };
7270
+ }
7271
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
7272
+ const resolved = resolveJsonPath(root, assertion.path);
7273
+ const errors = [];
7274
+ const result = {
7275
+ label: assertion.label || assertion.path,
7276
+ path: assertion.path,
7277
+ ok: true,
7278
+ exists: resolved.exists,
7279
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
7280
+ };
7281
+ if (resolved.exists) result.observed = toJsonValue(resolved.value);
7282
+ if (resolved.error) errors.push(resolved.error);
7283
+ if (hasOwn(assertion, "exists")) {
7284
+ result.expected_exists = assertion.exists;
7285
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
7286
+ }
7287
+ if (hasOwn(assertion, "type")) {
7288
+ result.type = assertion.type;
7289
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
7290
+ }
7291
+ if (hasOwn(assertion, "equals")) {
7292
+ result.equals = assertion.equals;
7293
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
7294
+ }
7295
+ if (hasOwn(assertion, "not_equals")) {
7296
+ result.not_equals = assertion.not_equals;
7297
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
7298
+ }
7299
+ if (hasOwn(assertion, "contains")) {
7300
+ result.contains = assertion.contains;
7301
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
7302
+ }
7303
+ result.ok = errors.length === 0;
7304
+ if (errors.length) result.errors = errors;
7305
+ return result;
7306
+ }
7307
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
7308
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
7309
+ if (!expected.length) return [];
7310
+ let parsed;
7311
+ try {
7312
+ parsed = JSON.parse(bodyText);
7313
+ } catch (error) {
7314
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
7315
+ return expected.map((assertion) => ({
7316
+ label: assertion.label || assertion.path,
7317
+ path: assertion.path,
7318
+ ok: false,
7319
+ exists: false,
7320
+ observed_type: "missing",
7321
+ errors: [message]
7322
+ }));
7323
+ }
7324
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
7325
+ }
7173
7326
  function compactProfileSetupSummaryText(value, limit = 160) {
7174
7327
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
7175
7328
  if (!text) return void 0;
@@ -7533,6 +7686,20 @@ function normalizeNetworkMock(input, index) {
7533
7686
  if (maxHitCount !== void 0 && effectiveRequiredHitCount > maxHitCount) {
7534
7687
  throw new Error(`target.network_mocks[${index}].max_hit_count cannot be less than its required hit count.`);
7535
7688
  }
7689
+ const sequenceScopeInput = stringValue2(
7690
+ input.sequence_scope ?? input.sequenceScope ?? input.response_sequence_scope ?? input.responseSequenceScope
7691
+ );
7692
+ let sequenceScope;
7693
+ if (sequenceScopeInput) {
7694
+ const normalizedScope = sequenceScopeInput.toLowerCase().replace(/[-\s]+/g, "_");
7695
+ if (normalizedScope === "global" || normalizedScope === "profile" || normalizedScope === "run") {
7696
+ sequenceScope = "global";
7697
+ } else if (normalizedScope === "viewport" || normalizedScope === "per_viewport" || normalizedScope === "viewport_scoped") {
7698
+ sequenceScope = "viewport";
7699
+ } else {
7700
+ throw new Error(`target.network_mocks[${index}].sequence_scope must be "global" or "viewport".`);
7701
+ }
7702
+ }
7536
7703
  return {
7537
7704
  ...payload,
7538
7705
  label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
@@ -7540,6 +7707,7 @@ function normalizeNetworkMock(input, index) {
7540
7707
  method: stringValue2(input.method)?.toUpperCase(),
7541
7708
  responses,
7542
7709
  repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
7710
+ sequence_scope: sequenceScope,
7543
7711
  required_hit_count: requiredHitCount,
7544
7712
  max_hit_count: maxHitCount,
7545
7713
  forbidden,
@@ -7809,6 +7977,46 @@ function validateRegexPatterns(patterns, label) {
7809
7977
  }
7810
7978
  }
7811
7979
  }
7980
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
7981
+ if (value === void 0) return void 0;
7982
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
7983
+ if (!value.length) throw new Error(`${label} must not be empty.`);
7984
+ return value.map((item, index) => {
7985
+ const itemLabel = `${label}[${index}]`;
7986
+ if (typeof item === "string") {
7987
+ const path8 = stringValue2(item);
7988
+ if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
7989
+ return { path: path8, exists: true };
7990
+ }
7991
+ if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
7992
+ const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
7993
+ if (!path7) throw new Error(`${itemLabel}.path is required.`);
7994
+ const assertion = {
7995
+ label: stringValue2(item.label),
7996
+ path: path7
7997
+ };
7998
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
7999
+ if (exists !== void 0) assertion.exists = exists;
8000
+ const type = stringValue2(valueFromOwn(item, "type", "value_type", "valueType"));
8001
+ if (type !== void 0) {
8002
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
8003
+ if (!allowedTypes.includes(type)) {
8004
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
8005
+ }
8006
+ assertion.type = type;
8007
+ }
8008
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
8009
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
8010
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
8011
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
8012
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
8013
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
8014
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
8015
+ assertion.exists = true;
8016
+ }
8017
+ return assertion;
8018
+ });
8019
+ }
7812
8020
  function isDialogCountCheckType(type) {
7813
8021
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
7814
8022
  }
@@ -7908,6 +8116,10 @@ function normalizeCheck(input, index) {
7908
8116
  `checks[${index}] body_not_patterns`
7909
8117
  ) : void 0;
7910
8118
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
8119
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
8120
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
8121
+ `checks[${index}] body_json_assertions`
8122
+ ) : void 0;
7911
8123
  if (isLinkStatusCheck) {
7912
8124
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
7913
8125
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -7933,6 +8145,7 @@ function normalizeCheck(input, index) {
7933
8145
  body_contains: bodyContains,
7934
8146
  body_not_contains: bodyNotContains,
7935
8147
  body_not_patterns: bodyNotPatterns,
8148
+ body_json_assertions: bodyJsonAssertions,
7936
8149
  expected_texts: expectedTexts,
7937
8150
  link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
7938
8151
  source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
@@ -8139,6 +8352,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
8139
8352
  const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
8140
8353
  return forbidden.filter((pattern) => observed[pattern] !== false);
8141
8354
  }
8355
+ function httpStatusBodyJsonAssertionFailures(result, check) {
8356
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
8357
+ if (!expected.length) return [];
8358
+ if (!Array.isArray(result.body_json_assertions)) {
8359
+ return expected.map((assertion) => ({
8360
+ label: assertion.label || assertion.path,
8361
+ path: assertion.path,
8362
+ ok: false,
8363
+ exists: false,
8364
+ observed_type: "missing",
8365
+ errors: ["body_json_assertions evidence missing"]
8366
+ }));
8367
+ }
8368
+ return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
8369
+ label: stringValue2(assertion.label) || stringValue2(assertion.path) || "json assertion",
8370
+ path: stringValue2(assertion.path) || "",
8371
+ ok: false,
8372
+ exists: assertion.exists === true,
8373
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
8374
+ observed_type: stringValue2(assertion.observed_type) || "missing",
8375
+ expected_exists: booleanValue(assertion.expected_exists),
8376
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
8377
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
8378
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
8379
+ type: stringValue2(assertion.type),
8380
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
8381
+ }));
8382
+ }
8142
8383
  function linkStatusResultOk(result, check) {
8143
8384
  const status = numberValue(result.status);
8144
8385
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -8156,6 +8397,7 @@ function linkStatusResultOk(result, check) {
8156
8397
  if (httpStatusBodyContainsFailures(result, check).length) return false;
8157
8398
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
8158
8399
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
8400
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
8159
8401
  return true;
8160
8402
  }
8161
8403
  function responseHeader(response, name) {
@@ -8224,7 +8466,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8224
8466
  statusText = typeof response.statusText === "string" ? response.statusText : "";
8225
8467
  result.content_type = responseHeader(response, "content-type");
8226
8468
  result.content_length = responseContentLength(response);
8227
- const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length);
8469
+ const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length) || Boolean(check.body_json_assertions?.length);
8228
8470
  if (shouldReadBody && method !== "HEAD") {
8229
8471
  const body = await responseBodyText(response);
8230
8472
  result.bytes = body.bytes;
@@ -8237,6 +8479,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8237
8479
  if (check.body_not_patterns?.length) {
8238
8480
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
8239
8481
  }
8482
+ if (check.body_json_assertions?.length) {
8483
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
8484
+ }
8240
8485
  }
8241
8486
  } catch (caught) {
8242
8487
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -8245,6 +8490,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8245
8490
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
8246
8491
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
8247
8492
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
8493
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
8248
8494
  const ok = !error && linkStatusResultOk(result, check);
8249
8495
  return {
8250
8496
  index,
@@ -8263,7 +8509,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8263
8509
  body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
8264
8510
  body_not_contains_found: bodyNotContainsFound,
8265
8511
  body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
8266
- body_not_patterns_found: bodyNotPatternsFound
8512
+ body_not_patterns_found: bodyNotPatternsFound,
8513
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
8514
+ body_json_assertions_failed: bodyJsonAssertionsFailed
8267
8515
  };
8268
8516
  }
8269
8517
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -8308,6 +8556,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
8308
8556
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
8309
8557
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
8310
8558
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
8559
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
8311
8560
  if (!linkStatusResultOk(statusEvidence, check)) {
8312
8561
  failures.push({
8313
8562
  code: "http_status_failed",
@@ -8326,6 +8575,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8326
8575
  body_not_contains_found: bodyNotContainsFound,
8327
8576
  body_not_patterns: check.body_not_patterns ?? null,
8328
8577
  body_not_patterns_found: bodyNotPatternsFound,
8578
+ body_json_assertions: check.body_json_assertions ?? null,
8579
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8329
8580
  body_sample: stringValue2(statusEvidence.body_sample) ?? null
8330
8581
  });
8331
8582
  }
@@ -8347,6 +8598,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8347
8598
  body_not_contains_found: bodyNotContainsFound,
8348
8599
  body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
8349
8600
  body_not_patterns_found: bodyNotPatternsFound,
8601
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
8602
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8350
8603
  body_sample: stringValue2(statusEvidence.body_sample) ?? null,
8351
8604
  failures
8352
8605
  };
@@ -8983,6 +9236,7 @@ function assessCheckFromEvidence(check, evidence) {
8983
9236
  body_contains: check.body_contains ?? [],
8984
9237
  body_not_contains: check.body_not_contains ?? [],
8985
9238
  body_not_patterns: check.body_not_patterns ?? [],
9239
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
8986
9240
  viewports: summaries.map((summary) => toJsonValue(summary)),
8987
9241
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
8988
9242
  },
@@ -9710,6 +9964,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
9710
9964
  : {};
9711
9965
  return forbidden.filter((pattern) => observed[pattern] !== false);
9712
9966
  }
9967
+ function httpStatusBodyJsonAssertionFailures(result, check) {
9968
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
9969
+ if (!expected.length) return [];
9970
+ if (!Array.isArray(result.body_json_assertions)) {
9971
+ return expected.map((assertion) => ({
9972
+ label: assertion.label || assertion.path,
9973
+ path: assertion.path,
9974
+ ok: false,
9975
+ exists: false,
9976
+ observed_type: "missing",
9977
+ errors: ["body_json_assertions evidence missing"],
9978
+ }));
9979
+ }
9980
+ return result.body_json_assertions
9981
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
9982
+ .map((assertion) => ({
9983
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
9984
+ path: typeof assertion.path === "string" ? assertion.path : "",
9985
+ ok: false,
9986
+ exists: assertion.exists === true,
9987
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
9988
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
9989
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
9990
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
9991
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
9992
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
9993
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
9994
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
9995
+ }));
9996
+ }
9713
9997
  function linkStatusResultOk(result, check) {
9714
9998
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
9715
9999
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -9727,6 +10011,7 @@ function linkStatusResultOk(result, check) {
9727
10011
  if (httpStatusBodyContainsFailures(result, check).length) return false;
9728
10012
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
9729
10013
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
10014
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
9730
10015
  return true;
9731
10016
  }
9732
10017
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -9747,6 +10032,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
9747
10032
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
9748
10033
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
9749
10034
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
10035
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
9750
10036
  if (!linkStatusResultOk(statusEvidence, check)) {
9751
10037
  failures.push({
9752
10038
  code: "http_status_failed",
@@ -9765,6 +10051,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9765
10051
  body_not_contains_found: bodyNotContainsFound,
9766
10052
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
9767
10053
  body_not_patterns_found: bodyNotPatternsFound,
10054
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
10055
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9768
10056
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9769
10057
  });
9770
10058
  }
@@ -9792,6 +10080,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9792
10080
  ? statusEvidence.body_not_patterns
9793
10081
  : null,
9794
10082
  body_not_patterns_found: bodyNotPatternsFound,
10083
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
10084
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9795
10085
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9796
10086
  failures,
9797
10087
  };
@@ -11175,6 +11465,7 @@ async function setupLocatorVisible(locator, index) {
11175
11465
  async function registerNetworkMocks(mocks) {
11176
11466
  for (const mock of mocks || []) {
11177
11467
  let hitCount = 0;
11468
+ const scopedHitCounts = {};
11178
11469
  await page.route(mock.url, async (route) => {
11179
11470
  const request = route.request();
11180
11471
  const method = request.method ? request.method() : "";
@@ -11190,8 +11481,13 @@ async function registerNetworkMocks(mocks) {
11190
11481
  const responses = Array.isArray(mock.responses) ? mock.responses : [];
11191
11482
  const hitIndex = hitCount;
11192
11483
  hitCount += 1;
11484
+ const sequenceScope = mock.sequence_scope === "viewport" ? "viewport" : "global";
11485
+ const viewportName = activeViewportName || null;
11486
+ const sequenceScopeKey = sequenceScope === "viewport" ? (viewportName || "__unknown_viewport__") : "__global__";
11487
+ const sequenceHitIndex = sequenceScope === "viewport" ? (scopedHitCounts[sequenceScopeKey] || 0) : hitIndex;
11488
+ if (sequenceScope === "viewport") scopedHitCounts[sequenceScopeKey] = sequenceHitIndex + 1;
11193
11489
  const sequenceResponseIndex = responses.length
11194
- ? (mock.repeat_responses ? hitIndex % responses.length : Math.min(hitIndex, responses.length - 1))
11490
+ ? (mock.repeat_responses ? sequenceHitIndex % responses.length : Math.min(sequenceHitIndex, responses.length - 1))
11195
11491
  : null;
11196
11492
  let responseIndex = sequenceResponseIndex;
11197
11493
  let responseSelection = responseIndex === null ? "mock" : "sequence";
@@ -11226,11 +11522,14 @@ async function registerNetworkMocks(mocks) {
11226
11522
  label: mock.label,
11227
11523
  response_label: response.label || null,
11228
11524
  hit_index: hitIndex,
11525
+ sequence_hit_index: responseIndex === null ? undefined : sequenceHitIndex,
11526
+ sequence_scope: responseIndex === null ? undefined : sequenceScope,
11527
+ viewport: viewportName,
11229
11528
  response_index: responseIndex,
11230
11529
  sequence_response_index: responseSelection === "request_body" ? sequenceResponseIndex : undefined,
11231
11530
  response_selection: responseIndex === null ? null : responseSelection,
11232
- sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && hitIndex >= responses.length,
11233
- sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && hitIndex >= responses.length,
11531
+ sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && sequenceHitIndex >= responses.length,
11532
+ sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && sequenceHitIndex >= responses.length,
11234
11533
  url: request.url(),
11235
11534
  method,
11236
11535
  };
@@ -11272,6 +11571,7 @@ async function registerNetworkMocks(mocks) {
11272
11571
  });
11273
11572
  }
11274
11573
  }
11574
+ let activeViewportName = null;
11275
11575
  async function executeSetupAction(action, ordinal, viewport) {
11276
11576
  const type = setupActionType(action);
11277
11577
  const frameSelector = setupFrameSelector(action);
@@ -11884,6 +12184,155 @@ function linkProbeResponseFields(response, method) {
11884
12184
  content_length: contentLength,
11885
12185
  };
11886
12186
  }
12187
+ function jsonProbeValueType(value) {
12188
+ if (value === null) return "null";
12189
+ if (Array.isArray(value)) return "array";
12190
+ if (typeof value === "boolean") return "boolean";
12191
+ if (typeof value === "number") return "number";
12192
+ if (typeof value === "string") return "string";
12193
+ return "object";
12194
+ }
12195
+ function jsonProbeDeepEqual(left, right) {
12196
+ if (left === right) return true;
12197
+ if (typeof left !== typeof right) return false;
12198
+ if (left === null || right === null) return left === right;
12199
+ if (typeof left !== "object" || typeof right !== "object") return false;
12200
+ if (Array.isArray(left) || Array.isArray(right)) {
12201
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
12202
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
12203
+ }
12204
+ const leftKeys = Object.keys(left).sort();
12205
+ const rightKeys = Object.keys(right).sort();
12206
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
12207
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
12208
+ }
12209
+ function jsonProbeContains(observed, expected) {
12210
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
12211
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
12212
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
12213
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
12214
+ }
12215
+ return false;
12216
+ }
12217
+ function parseJsonProbePathSegments(path) {
12218
+ let input = String(path || "").trim();
12219
+ if (!input) throw new Error("path is empty");
12220
+ if (input === "$") return [];
12221
+ if (input.startsWith("$.")) input = input.slice(2);
12222
+ else if (input.startsWith("$[")) input = input.slice(1);
12223
+ const segments = [];
12224
+ let token = "";
12225
+ const pushToken = () => {
12226
+ if (!token) return;
12227
+ segments.push(token);
12228
+ token = "";
12229
+ };
12230
+ for (let index = 0; index < input.length; index += 1) {
12231
+ const char = input[index];
12232
+ if (char === ".") {
12233
+ pushToken();
12234
+ continue;
12235
+ }
12236
+ if (char !== "[") {
12237
+ token += char;
12238
+ continue;
12239
+ }
12240
+ pushToken();
12241
+ const closeIndex = input.indexOf("]", index + 1);
12242
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
12243
+ const bracket = input.slice(index + 1, closeIndex).trim();
12244
+ if (!bracket) throw new Error("empty bracket at " + index);
12245
+ if (/^\d+$/.test(bracket)) {
12246
+ segments.push(Number(bracket));
12247
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
12248
+ const quoted = bracket.startsWith("'")
12249
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
12250
+ : bracket;
12251
+ segments.push(String(JSON.parse(quoted)));
12252
+ } else {
12253
+ segments.push(bracket);
12254
+ }
12255
+ index = closeIndex;
12256
+ }
12257
+ pushToken();
12258
+ return segments;
12259
+ }
12260
+ function resolveJsonProbePath(root, path) {
12261
+ let segments;
12262
+ try {
12263
+ segments = parseJsonProbePathSegments(path);
12264
+ } catch (error) {
12265
+ return { exists: false, error: String(error && error.message ? error.message : error) };
12266
+ }
12267
+ let current = root;
12268
+ for (const segment of segments) {
12269
+ if (typeof segment === "number") {
12270
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
12271
+ current = current[segment];
12272
+ continue;
12273
+ }
12274
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
12275
+ return { exists: false };
12276
+ }
12277
+ current = current[segment];
12278
+ }
12279
+ return { exists: true, value: current };
12280
+ }
12281
+ function evaluateJsonProbeAssertion(root, assertion) {
12282
+ const resolved = resolveJsonProbePath(root, assertion.path);
12283
+ const errors = [];
12284
+ const result = {
12285
+ label: assertion.label || assertion.path,
12286
+ path: assertion.path,
12287
+ ok: true,
12288
+ exists: resolved.exists,
12289
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
12290
+ };
12291
+ if (resolved.exists) result.observed = resolved.value;
12292
+ if (resolved.error) errors.push(resolved.error);
12293
+ if (Object.hasOwn(assertion, "exists")) {
12294
+ result.expected_exists = assertion.exists;
12295
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
12296
+ }
12297
+ if (Object.hasOwn(assertion, "type")) {
12298
+ result.type = assertion.type;
12299
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
12300
+ }
12301
+ if (Object.hasOwn(assertion, "equals")) {
12302
+ result.equals = assertion.equals;
12303
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
12304
+ }
12305
+ if (Object.hasOwn(assertion, "not_equals")) {
12306
+ result.not_equals = assertion.not_equals;
12307
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
12308
+ }
12309
+ if (Object.hasOwn(assertion, "contains")) {
12310
+ result.contains = assertion.contains;
12311
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
12312
+ }
12313
+ result.ok = errors.length === 0;
12314
+ if (errors.length) result.errors = errors;
12315
+ return result;
12316
+ }
12317
+ function evaluateJsonProbeAssertions(text, assertions) {
12318
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
12319
+ if (!expected.length) return [];
12320
+ let parsed;
12321
+ try {
12322
+ parsed = JSON.parse(text);
12323
+ } catch (error) {
12324
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
12325
+ return expected.map((assertion) => ({
12326
+ label: assertion.label || assertion.path,
12327
+ path: assertion.path,
12328
+ ok: false,
12329
+ exists: false,
12330
+ observed_type: "missing",
12331
+ errors: [message],
12332
+ }));
12333
+ }
12334
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
12335
+ }
11887
12336
  async function collectHttpStatus(check) {
11888
12337
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
11889
12338
  const method = httpStatusMethod(check);
@@ -11900,6 +12349,7 @@ async function collectHttpStatus(check) {
11900
12349
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
11901
12350
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
11902
12351
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
12352
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
11903
12353
  const options = {
11904
12354
  method,
11905
12355
  redirect: "follow",
@@ -11925,17 +12375,18 @@ async function collectHttpStatus(check) {
11925
12375
  Object.assign(result, linkProbeResponseFields(response, method));
11926
12376
  result.url = url;
11927
12377
  result.status_text = response.statusText || "";
11928
- const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0;
12378
+ const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0 || bodyJsonAssertions.length > 0;
11929
12379
  if (shouldReadBody) {
11930
12380
  try {
11931
12381
  const buffer = await response.arrayBuffer();
11932
12382
  result.bytes = buffer.byteLength;
11933
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
12383
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
11934
12384
  const text = new TextDecoder().decode(buffer);
11935
12385
  result.body_sample = text.slice(0, 1000);
11936
12386
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
11937
12387
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
11938
12388
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
12389
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
11939
12390
  }
11940
12391
  } catch (error) {
11941
12392
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -11948,6 +12399,7 @@ async function collectHttpStatus(check) {
11948
12399
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
11949
12400
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
11950
12401
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
12402
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
11951
12403
  && !result.error;
11952
12404
  return result;
11953
12405
  } catch (error) {
@@ -12534,6 +12986,7 @@ async function collectRouteInventory(check, viewport) {
12534
12986
  };
12535
12987
  }
12536
12988
  async function captureViewport(viewport) {
12989
+ activeViewportName = viewport && viewport.name ? viewport.name : null;
12537
12990
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
12538
12991
  let httpStatus = null;
12539
12992
  let navigationError;
@@ -13659,6 +14112,21 @@ function profileHttpStatusAssertionKeys(evidence, viewports, field) {
13659
14112
  }
13660
14113
  return [...keys];
13661
14114
  }
14115
+ function profileHttpStatusJsonAssertionCount(viewports) {
14116
+ if (!viewports.length) return void 0;
14117
+ let passed = 0;
14118
+ let total = 0;
14119
+ for (const viewport of viewports) {
14120
+ if (!Array.isArray(viewport.body_json_assertions)) continue;
14121
+ for (const assertion of viewport.body_json_assertions) {
14122
+ const record = cliRecord(assertion);
14123
+ if (!record) continue;
14124
+ total += 1;
14125
+ if (record.ok === true) passed += 1;
14126
+ }
14127
+ }
14128
+ return total ? { passed, total } : void 0;
14129
+ }
13662
14130
  function profileHttpStatusSummaryMarkdown(result) {
13663
14131
  const httpStatusChecks = result.checks.filter((check) => check.type === "http_status");
13664
14132
  const lines = [];
@@ -13689,10 +14157,12 @@ function profileHttpStatusSummaryMarkdown(result) {
13689
14157
  profileHttpStatusAssertionKeys(evidence, viewports, "body_not_patterns"),
13690
14158
  false
13691
14159
  );
14160
+ const bodyJsonAssertions = profileHttpStatusJsonAssertionCount(viewports);
13692
14161
  const bodyParts = [
13693
14162
  bodyContains ? `body_contains ${bodyContains.passed}/${bodyContains.total}` : "",
13694
14163
  bodyNotContains ? `body_not_contains clean ${bodyNotContains.passed}/${bodyNotContains.total}` : "",
13695
- bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : ""
14164
+ bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : "",
14165
+ bodyJsonAssertions ? `body_json_assertions ${bodyJsonAssertions.passed}/${bodyJsonAssertions.total}` : ""
13696
14166
  ].filter(Boolean);
13697
14167
  lines.push(
13698
14168
  `- ${label}: ${method}${url ? ` ${markdownInlineCode(url)}` : ""}, statuses ${statuses.length ? statuses.join("/") : "unknown"}${bodyParts.length ? `, ${bodyParts.join(", ")}` : ""}, failures ${failedTotal}`