@validate.qa/runner 1.0.67 → 1.0.69

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.
Files changed (3) hide show
  1. package/dist/cli.js +498 -260
  2. package/dist/cli.mjs +494 -256
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -983,7 +983,7 @@ var require_mobile_source_parser = __commonJS({
983
983
  const value = nonEmpty3(attrs.value);
984
984
  const label = name ?? labelAttr ?? value;
985
985
  const accessibilityId = name;
986
- const text = value && value !== label ? value : labelAttr && labelAttr !== label ? labelAttr : void 0;
986
+ const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
987
987
  const bounds = parseIOSBounds(attrs);
988
988
  const enabled = attrBoolDefaultTrue(attrs.enabled);
989
989
  const visible = attrBoolDefaultTrue(attrs.visible);
@@ -1130,6 +1130,42 @@ var require_mobile_source_parser = __commonJS({
1130
1130
  }
1131
1131
  }
1132
1132
  }
1133
+ function annotateIdentityUniqueness(elements) {
1134
+ const a11yCounts = /* @__PURE__ */ new Map();
1135
+ const ridCounts = /* @__PURE__ */ new Map();
1136
+ const textCounts = /* @__PURE__ */ new Map();
1137
+ const bump = (map2, value) => {
1138
+ const key = value?.trim();
1139
+ if (!key)
1140
+ return;
1141
+ map2.set(key, (map2.get(key) ?? 0) + 1);
1142
+ };
1143
+ for (const element of elements) {
1144
+ bump(a11yCounts, element.accessibilityId);
1145
+ bump(ridCounts, element.resourceId);
1146
+ const identityValues = new Set([element.text, element.accessibilityId].map((value) => value?.trim()).filter((value) => Boolean(value)));
1147
+ for (const value of identityValues)
1148
+ bump(textCounts, value);
1149
+ }
1150
+ const isUnique = (map2, value) => {
1151
+ const key = value?.trim();
1152
+ if (!key)
1153
+ return void 0;
1154
+ return (map2.get(key) ?? 0) <= 1;
1155
+ };
1156
+ for (const element of elements) {
1157
+ const accessibilityId = isUnique(a11yCounts, element.accessibilityId);
1158
+ const resourceId = isUnique(ridCounts, element.resourceId);
1159
+ const text = isUnique(textCounts, element.text);
1160
+ if (accessibilityId === void 0 && resourceId === void 0 && text === void 0)
1161
+ continue;
1162
+ element.identityUnique = {
1163
+ ...accessibilityId !== void 0 ? { accessibilityId } : {},
1164
+ ...resourceId !== void 0 ? { resourceId } : {},
1165
+ ...text !== void 0 ? { text } : {}
1166
+ };
1167
+ }
1168
+ }
1133
1169
  function flatten(root, platform3) {
1134
1170
  const out = [];
1135
1171
  const stack = [];
@@ -1175,6 +1211,7 @@ var require_mobile_source_parser = __commonJS({
1175
1211
  }
1176
1212
  }
1177
1213
  markRepeatedBottomBarNavigation(out);
1214
+ annotateIdentityUniqueness(out);
1178
1215
  return out;
1179
1216
  }
1180
1217
  function fnv1a(input) {
@@ -1256,6 +1293,13 @@ var require_mobile_source_parser = __commonJS({
1256
1293
  function isEditableElement(el) {
1257
1294
  return EDITABLE_CLASS_RE.test(el.role.trim());
1258
1295
  }
1296
+ var STATE_VALUE_CLASS_RE = /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/;
1297
+ function isStateValueRole(role) {
1298
+ return STATE_VALUE_CLASS_RE.test(role.trim());
1299
+ }
1300
+ function isStateValueElement(el) {
1301
+ return isStateValueRole(el.role);
1302
+ }
1259
1303
  function xpathAttrLiteral(value) {
1260
1304
  if (!value.includes("'"))
1261
1305
  return `'${value}'`;
@@ -1266,17 +1310,18 @@ var require_mobile_source_parser = __commonJS({
1266
1310
  function semanticXPath(el, platform3) {
1267
1311
  const cls = nonBlank(el.role) ?? "*";
1268
1312
  const a11y = nonBlank(el.accessibilityId);
1269
- if (a11y) {
1270
- return platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]`;
1271
- }
1272
- const rid = nonBlank(el.resourceId);
1273
- if (platform3 === "ANDROID" && rid)
1274
- return `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]`;
1275
- const text = nonBlank(el.text);
1276
- if (text && !isEditableElement(el)) {
1277
- return platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]`;
1278
- }
1279
- return void 0;
1313
+ const rid = platform3 === "ANDROID" ? nonBlank(el.resourceId) : void 0;
1314
+ const text = isEditableElement(el) || isStateValueElement(el) ? void 0 : nonBlank(el.text);
1315
+ const a11yXpath = a11y ? platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]` : void 0;
1316
+ const ridXpath = rid ? `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]` : void 0;
1317
+ const textXpath = text ? platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]` : void 0;
1318
+ const candidates = [
1319
+ { xpath: a11yXpath, unique: el.identityUnique?.accessibilityId },
1320
+ { xpath: ridXpath, unique: el.identityUnique?.resourceId },
1321
+ { xpath: textXpath, unique: el.identityUnique?.text }
1322
+ ];
1323
+ const uniqueFirst = candidates.find((c) => c.xpath && c.unique !== false);
1324
+ return (uniqueFirst ?? candidates.find((c) => c.xpath))?.xpath;
1280
1325
  }
1281
1326
  function nonBlank(value) {
1282
1327
  if (typeof value !== "string")
@@ -1285,30 +1330,40 @@ var require_mobile_source_parser = __commonJS({
1285
1330
  return trimmed.length > 0 ? trimmed : void 0;
1286
1331
  }
1287
1332
  function buildLocatorBundle(el, platform3) {
1333
+ const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
1288
1334
  const bundle = {
1289
1335
  // Attribute-anchored xpath when the element has identity; the absolute
1290
1336
  // indexed path only for identity-less elements (nothing else can find them).
1291
1337
  xpath: semanticXPath(el, platform3) ?? el.xpath,
1292
1338
  className: el.role
1293
1339
  };
1294
- if (el.accessibilityId)
1340
+ const a11yUnique = el.identityUnique?.accessibilityId !== false;
1341
+ const ridUnique = el.identityUnique?.resourceId !== false;
1342
+ const textUnique = el.identityUnique?.text !== false;
1343
+ const hasUniqueAlternativeToA11y = Boolean(el.resourceId && platform3 === "ANDROID" && ridUnique || durableText && textUnique);
1344
+ const hasUniqueAlternativeToRid = Boolean(el.accessibilityId && a11yUnique || durableText && textUnique);
1345
+ const includeA11y = Boolean(el.accessibilityId && (a11yUnique || !hasUniqueAlternativeToA11y));
1346
+ const includeRid = Boolean(platform3 === "ANDROID" && el.resourceId && (ridUnique || !hasUniqueAlternativeToRid));
1347
+ if (includeA11y && el.accessibilityId)
1295
1348
  bundle.accessibilityId = el.accessibilityId;
1296
- if (platform3 === "ANDROID" && el.resourceId)
1349
+ if (includeRid && el.resourceId)
1297
1350
  bundle.resourceId = el.resourceId;
1298
- if (el.text && !isEditableElement(el))
1299
- bundle.text = el.text;
1300
- if (platform3 === "ANDROID" && el.accessibilityId)
1351
+ if (durableText)
1352
+ bundle.text = durableText;
1353
+ if (includeA11y && platform3 === "ANDROID" && el.accessibilityId)
1301
1354
  bundle.contentDesc = el.accessibilityId;
1302
1355
  return bundle;
1303
1356
  }
1304
1357
  function preferredLocator(el) {
1305
- if (el.accessibilityId)
1306
- return el.accessibilityId;
1307
- if (el.resourceId)
1308
- return el.resourceId;
1309
- if (el.text && !isEditableElement(el))
1310
- return el.text;
1311
- return el.xpath;
1358
+ const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
1359
+ const candidates = [
1360
+ { value: el.accessibilityId, unique: el.identityUnique?.accessibilityId },
1361
+ { value: el.resourceId, unique: el.identityUnique?.resourceId },
1362
+ { value: durableText, unique: el.identityUnique?.text }
1363
+ ];
1364
+ const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
1365
+ const chosen = (uniqueFirst ?? candidates.find((c) => c.value))?.value;
1366
+ return chosen ?? el.xpath;
1312
1367
  }
1313
1368
  function buildScreenSignal(elements, root, platform3) {
1314
1369
  const signal = {
@@ -1971,6 +2026,11 @@ function isEditableMobileClassName(className) {
1971
2026
  return false;
1972
2027
  return /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/.test(className.trim());
1973
2028
  }
2029
+ function isStateValueMobileClassName(className) {
2030
+ if (!className)
2031
+ return false;
2032
+ return /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/.test(className.trim());
2033
+ }
1974
2034
  function mobileCredentialPlaceholder(key) {
1975
2035
  return `{{${key}}}`;
1976
2036
  }
@@ -2100,9 +2160,10 @@ function buildStepTarget(metadata, selector) {
2100
2160
  locatorBundle.resourceId,
2101
2161
  // For editable fields the text attribute IS the currently-typed value — a
2102
2162
  // self-invalidating locator that only matches while the recorded text is
2103
- // still in the field (and it can leak typed input into locators). Static
2104
- // elements keep their text fallback.
2105
- isEditableMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
2163
+ // still in the field (and it can leak typed input into locators). State
2164
+ // controls' text is their current state ("1"/"ON") and collides with every
2165
+ // same-state sibling. Static elements keep their text fallback.
2166
+ isEditableMobileClassName(locatorBundle.className) || isStateValueMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
2106
2167
  locatorBundle.contentDesc,
2107
2168
  locatorBundle.xpath,
2108
2169
  selector
@@ -2258,7 +2319,9 @@ function mobileLocatorBundleHasIdentity(bundle) {
2258
2319
  return false;
2259
2320
  if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
2260
2321
  return true;
2261
- if (nonEmpty(bundle.text) || nonEmpty(bundle.contentDesc))
2322
+ if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
2323
+ return true;
2324
+ if (nonEmpty(bundle.contentDesc))
2262
2325
  return true;
2263
2326
  const xpath = nonEmpty(bundle.xpath);
2264
2327
  return !!xpath && isIdentityMobileXpath(xpath);
@@ -2279,23 +2342,27 @@ function selectorCandidatesForStep(step) {
2279
2342
  candidates.push(`~${bundle.accessibilityId}`);
2280
2343
  if (nonEmpty(bundle.resourceId))
2281
2344
  candidates.push(`id=${bundle.resourceId}`);
2345
+ if (nonEmpty(bundle.xpath))
2346
+ candidates.push(bundle.xpath);
2282
2347
  const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
2283
- const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
2348
+ const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
2284
2349
  for (const value of [textIdentity, bundle.contentDesc]) {
2285
2350
  const trimmed = nonEmpty(value);
2286
2351
  if (trimmed)
2287
2352
  candidates.push(identityTextXpath(trimmed));
2288
2353
  }
2289
- if (nonEmpty(bundle.xpath))
2290
- candidates.push(bundle.xpath);
2291
2354
  }
2292
2355
  const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
2356
+ const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
2293
2357
  const pushValueCandidates = (value) => {
2294
2358
  candidates.push(selectorForValue(value));
2295
2359
  const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
2296
2360
  if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
2297
2361
  candidates.push(`id=${value}`);
2298
2362
  }
2363
+ if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
2364
+ candidates.push(identityTextXpath(value));
2365
+ }
2299
2366
  };
2300
2367
  if (nonEmpty(target.primary))
2301
2368
  pushValueCandidates(target.primary);
@@ -2990,7 +3057,8 @@ function isWebViewStep(step) {
2990
3057
  function hasCrossPlatformIdentity(bundle) {
2991
3058
  if (!bundle)
2992
3059
  return false;
2993
- return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
3060
+ const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
3061
+ return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
2994
3062
  }
2995
3063
  function stepIsDivergentForPlatform(step, platform3) {
2996
3064
  return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
@@ -3072,7 +3140,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
3072
3140
  function hasStableNativeIdentity(bundle) {
3073
3141
  if (!bundle)
3074
3142
  return false;
3075
- return Boolean(bundle.accessibilityId?.trim() || bundle.resourceId?.trim() || bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.xpath?.trim() && XPATH_ATTRIBUTE_EQUALITY_RE.test(bundle.xpath) && !/(?:contains|starts-with)\s*\(/i.test(bundle.xpath));
3143
+ const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
3144
+ return Boolean(bundle.accessibilityId?.trim() || bundle.resourceId?.trim() || durableText?.trim() || bundle.contentDesc?.trim() || bundle.xpath?.trim() && XPATH_ATTRIBUTE_EQUALITY_RE.test(bundle.xpath) && !/(?:contains|starts-with)\s*\(/i.test(bundle.xpath));
3076
3145
  }
3077
3146
  function isPositionalXpath(xpath) {
3078
3147
  const value = xpath?.trim();
@@ -3152,6 +3221,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
3152
3221
  var init_mobile_cross_platform = __esm({
3153
3222
  "../shared/dist/mobile-cross-platform.js"() {
3154
3223
  "use strict";
3224
+ init_mobile_test_generation();
3155
3225
  MOBILE_PLATFORMS = ["IOS", "ANDROID"];
3156
3226
  LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
3157
3227
  "tap",
@@ -6154,6 +6224,7 @@ __export(dist_exports, {
6154
6224
  isEditableMobileClassName: () => isEditableMobileClassName,
6155
6225
  isEmailFromIdentity: () => isEmailFromIdentity,
6156
6226
  isEnvironmentalError: () => isEnvironmentalError,
6227
+ isIdentityMobileXpath: () => isIdentityMobileXpath,
6157
6228
  isMobilePlatform: () => isMobilePlatform,
6158
6229
  isPricingPlan: () => isPricingPlan,
6159
6230
  isRunnableStep: () => isRunnableStep,
@@ -6161,6 +6232,7 @@ __export(dist_exports, {
6161
6232
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
6162
6233
  isSensitiveQueryKey: () => isSensitiveQueryKey,
6163
6234
  isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
6235
+ isStateValueMobileClassName: () => isStateValueMobileClassName,
6164
6236
  isSttModelForProvider: () => isSttModelForProvider,
6165
6237
  isTestReadyForPlatform: () => isTestReadyForPlatform,
6166
6238
  isTrialExpired: () => isTrialExpired,
@@ -7068,6 +7140,7 @@ var require_mobile_observation = __commonJS({
7068
7140
  exports.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
7069
7141
  exports.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
7070
7142
  exports.safeMobileElementLabel = safeMobileElementLabel;
7143
+ var mobile_source_parser_js_1 = require_mobile_source_parser();
7071
7144
  var DEFAULT_MAX_ELEMENTS = 60;
7072
7145
  var MAX_LABEL_LENGTH = 80;
7073
7146
  var EVIDENCE_LABEL_LIMIT = 40;
@@ -7158,7 +7231,8 @@ var require_mobile_observation = __commonJS({
7158
7231
  const locators = [];
7159
7232
  if (stableId)
7160
7233
  locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
7161
- const xpath = element.xpath?.trim();
7234
+ const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
7235
+ const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
7162
7236
  if (xpath)
7163
7237
  locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
7164
7238
  const collected = {
@@ -11224,7 +11298,7 @@ var require_thread_stream = __commonJS({
11224
11298
  var { version } = require_package();
11225
11299
  var { EventEmitter: EventEmitter2 } = __require("events");
11226
11300
  var { Worker } = __require("worker_threads");
11227
- var { join: join7 } = __require("path");
11301
+ var { join: join8 } = __require("path");
11228
11302
  var { pathToFileURL } = __require("url");
11229
11303
  var { wait } = require_wait();
11230
11304
  var {
@@ -11275,7 +11349,7 @@ var require_thread_stream = __commonJS({
11275
11349
  function createWorker(stream, opts) {
11276
11350
  const { filename, workerData } = opts;
11277
11351
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
11278
- const toExecute = bundlerOverrides["thread-stream-worker"] || join7(__dirname, "lib", "worker.js");
11352
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
11279
11353
  const worker = new Worker(toExecute, {
11280
11354
  ...opts.workerOpts,
11281
11355
  name: opts.workerOpts?.name || "thread-stream",
@@ -11743,7 +11817,7 @@ var require_transport = __commonJS({
11743
11817
  var { createRequire } = __require("module");
11744
11818
  var { existsSync: existsSync3 } = __require("fs");
11745
11819
  var getCallers = require_caller();
11746
- var { join: join7, isAbsolute, sep } = __require("path");
11820
+ var { join: join8, isAbsolute: isAbsolute2, sep } = __require("path");
11747
11821
  var { fileURLToPath } = __require("url");
11748
11822
  var sleep = require_atomic_sleep();
11749
11823
  var onExit = require_on_exit_leak_free();
@@ -11815,7 +11889,7 @@ var require_transport = __commonJS({
11815
11889
  return false;
11816
11890
  }
11817
11891
  }
11818
- return isAbsolute(path) && !existsSync3(path);
11892
+ return isAbsolute2(path) && !existsSync3(path);
11819
11893
  }
11820
11894
  function stripQuotes(value) {
11821
11895
  const first = value[0];
@@ -11896,7 +11970,7 @@ var require_transport = __commonJS({
11896
11970
  throw new Error("only one of target or targets can be specified");
11897
11971
  }
11898
11972
  if (targets) {
11899
- target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
11973
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
11900
11974
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
11901
11975
  return {
11902
11976
  ...dest,
@@ -11914,7 +11988,7 @@ var require_transport = __commonJS({
11914
11988
  });
11915
11989
  });
11916
11990
  } else if (pipeline) {
11917
- target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
11991
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
11918
11992
  options.pipelines = [pipeline.map((dest) => {
11919
11993
  return {
11920
11994
  ...dest,
@@ -11933,11 +12007,11 @@ var require_transport = __commonJS({
11933
12007
  return buildStream(fixTarget(target), options, worker, sync, name);
11934
12008
  function fixTarget(origin) {
11935
12009
  origin = bundlerOverrides[origin] || origin;
11936
- if (isAbsolute(origin) || origin.indexOf("file://") === 0) {
12010
+ if (isAbsolute2(origin) || origin.indexOf("file://") === 0) {
11937
12011
  return origin;
11938
12012
  }
11939
12013
  if (origin === "pino/file") {
11940
- return join7(__dirname, "..", "file.js");
12014
+ return join8(__dirname, "..", "file.js");
11941
12015
  }
11942
12016
  let fixTarget2;
11943
12017
  for (const filePath of callers) {
@@ -12918,7 +12992,7 @@ var require_safe_stable_stringify = __commonJS({
12918
12992
  return circularValue;
12919
12993
  }
12920
12994
  let res = "";
12921
- let join7 = ",";
12995
+ let join8 = ",";
12922
12996
  const originalIndentation = indentation;
12923
12997
  if (Array.isArray(value)) {
12924
12998
  if (value.length === 0) {
@@ -12932,7 +13006,7 @@ var require_safe_stable_stringify = __commonJS({
12932
13006
  indentation += spacer;
12933
13007
  res += `
12934
13008
  ${indentation}`;
12935
- join7 = `,
13009
+ join8 = `,
12936
13010
  ${indentation}`;
12937
13011
  }
12938
13012
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -12940,13 +13014,13 @@ ${indentation}`;
12940
13014
  for (; i < maximumValuesToStringify - 1; i++) {
12941
13015
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
12942
13016
  res += tmp2 !== void 0 ? tmp2 : "null";
12943
- res += join7;
13017
+ res += join8;
12944
13018
  }
12945
13019
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
12946
13020
  res += tmp !== void 0 ? tmp : "null";
12947
13021
  if (value.length - 1 > maximumBreadth) {
12948
13022
  const removedKeys = value.length - maximumBreadth - 1;
12949
- res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
13023
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
12950
13024
  }
12951
13025
  if (spacer !== "") {
12952
13026
  res += `
@@ -12967,7 +13041,7 @@ ${originalIndentation}`;
12967
13041
  let separator = "";
12968
13042
  if (spacer !== "") {
12969
13043
  indentation += spacer;
12970
- join7 = `,
13044
+ join8 = `,
12971
13045
  ${indentation}`;
12972
13046
  whitespace = " ";
12973
13047
  }
@@ -12981,13 +13055,13 @@ ${indentation}`;
12981
13055
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
12982
13056
  if (tmp !== void 0) {
12983
13057
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
12984
- separator = join7;
13058
+ separator = join8;
12985
13059
  }
12986
13060
  }
12987
13061
  if (keyLength > maximumBreadth) {
12988
13062
  const removedKeys = keyLength - maximumBreadth;
12989
13063
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
12990
- separator = join7;
13064
+ separator = join8;
12991
13065
  }
12992
13066
  if (spacer !== "" && separator.length > 1) {
12993
13067
  res = `
@@ -13028,7 +13102,7 @@ ${originalIndentation}`;
13028
13102
  }
13029
13103
  const originalIndentation = indentation;
13030
13104
  let res = "";
13031
- let join7 = ",";
13105
+ let join8 = ",";
13032
13106
  if (Array.isArray(value)) {
13033
13107
  if (value.length === 0) {
13034
13108
  return "[]";
@@ -13041,7 +13115,7 @@ ${originalIndentation}`;
13041
13115
  indentation += spacer;
13042
13116
  res += `
13043
13117
  ${indentation}`;
13044
- join7 = `,
13118
+ join8 = `,
13045
13119
  ${indentation}`;
13046
13120
  }
13047
13121
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -13049,13 +13123,13 @@ ${indentation}`;
13049
13123
  for (; i < maximumValuesToStringify - 1; i++) {
13050
13124
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
13051
13125
  res += tmp2 !== void 0 ? tmp2 : "null";
13052
- res += join7;
13126
+ res += join8;
13053
13127
  }
13054
13128
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
13055
13129
  res += tmp !== void 0 ? tmp : "null";
13056
13130
  if (value.length - 1 > maximumBreadth) {
13057
13131
  const removedKeys = value.length - maximumBreadth - 1;
13058
- res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
13132
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
13059
13133
  }
13060
13134
  if (spacer !== "") {
13061
13135
  res += `
@@ -13068,7 +13142,7 @@ ${originalIndentation}`;
13068
13142
  let whitespace = "";
13069
13143
  if (spacer !== "") {
13070
13144
  indentation += spacer;
13071
- join7 = `,
13145
+ join8 = `,
13072
13146
  ${indentation}`;
13073
13147
  whitespace = " ";
13074
13148
  }
@@ -13077,7 +13151,7 @@ ${indentation}`;
13077
13151
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
13078
13152
  if (tmp !== void 0) {
13079
13153
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
13080
- separator = join7;
13154
+ separator = join8;
13081
13155
  }
13082
13156
  }
13083
13157
  if (spacer !== "" && separator.length > 1) {
@@ -13135,20 +13209,20 @@ ${originalIndentation}`;
13135
13209
  indentation += spacer;
13136
13210
  let res2 = `
13137
13211
  ${indentation}`;
13138
- const join8 = `,
13212
+ const join9 = `,
13139
13213
  ${indentation}`;
13140
13214
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
13141
13215
  let i = 0;
13142
13216
  for (; i < maximumValuesToStringify - 1; i++) {
13143
13217
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
13144
13218
  res2 += tmp2 !== void 0 ? tmp2 : "null";
13145
- res2 += join8;
13219
+ res2 += join9;
13146
13220
  }
13147
13221
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
13148
13222
  res2 += tmp !== void 0 ? tmp : "null";
13149
13223
  if (value.length - 1 > maximumBreadth) {
13150
13224
  const removedKeys = value.length - maximumBreadth - 1;
13151
- res2 += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
13225
+ res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
13152
13226
  }
13153
13227
  res2 += `
13154
13228
  ${originalIndentation}`;
@@ -13164,16 +13238,16 @@ ${originalIndentation}`;
13164
13238
  return '"[Object]"';
13165
13239
  }
13166
13240
  indentation += spacer;
13167
- const join7 = `,
13241
+ const join8 = `,
13168
13242
  ${indentation}`;
13169
13243
  let res = "";
13170
13244
  let separator = "";
13171
13245
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
13172
13246
  if (isTypedArrayWithEntries(value)) {
13173
- res += stringifyTypedArray(value, join7, maximumBreadth);
13247
+ res += stringifyTypedArray(value, join8, maximumBreadth);
13174
13248
  keys = keys.slice(value.length);
13175
13249
  maximumPropertiesToStringify -= value.length;
13176
- separator = join7;
13250
+ separator = join8;
13177
13251
  }
13178
13252
  if (deterministic) {
13179
13253
  keys = sort(keys, comparator);
@@ -13184,13 +13258,13 @@ ${indentation}`;
13184
13258
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
13185
13259
  if (tmp !== void 0) {
13186
13260
  res += `${separator}${strEscape(key2)}: ${tmp}`;
13187
- separator = join7;
13261
+ separator = join8;
13188
13262
  }
13189
13263
  }
13190
13264
  if (keyLength > maximumBreadth) {
13191
13265
  const removedKeys = keyLength - maximumBreadth;
13192
13266
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
13193
- separator = join7;
13267
+ separator = join8;
13194
13268
  }
13195
13269
  if (separator !== "") {
13196
13270
  res = `
@@ -61866,11 +61940,11 @@ var require_core = __commonJS({
61866
61940
  function mapOuter(r, f) {
61867
61941
  return r.matched ? f(r) : r;
61868
61942
  }
61869
- function ab(pa, pb, join7) {
61870
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join7(ma.value, vb, data, i, j)));
61943
+ function ab(pa, pb, join8) {
61944
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join8(ma.value, vb, data, i, j)));
61871
61945
  }
61872
- function abc(pa, pb, pc, join7) {
61873
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join7(ma.value, mb.value, vc, data, i, j))));
61946
+ function abc(pa, pb, pc, join8) {
61947
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join8(ma.value, mb.value, vc, data, i, j))));
61874
61948
  }
61875
61949
  function action(f) {
61876
61950
  return (data, i) => {
@@ -78407,6 +78481,10 @@ var require_discover_ai_planner = __commonJS({
78407
78481
  if (context.appBrief) {
78408
78482
  sections.push(`## Application Brief
78409
78483
  ${context.appBrief.slice(0, 500)}`);
78484
+ }
78485
+ if (context.knowledgeContext && context.knowledgeContext.trim().length > 0) {
78486
+ sections.push(`## Known Facts
78487
+ ${context.knowledgeContext.slice(0, 4e3)}`);
78410
78488
  }
78411
78489
  sections.push(`## Base URL
78412
78490
  ${compressed.baseUrl}`);
@@ -83451,48 +83529,29 @@ ${statePacket}`, redactionCredentials) },
83451
83529
  log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
83452
83530
  return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
83453
83531
  }
83454
- if (platform3 !== "IOS") {
83455
- const appDataCleared = await driver.resetAppState?.().catch((err) => {
83456
- log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
83457
- return false;
83458
- }) ?? false;
83459
- if (appDataCleared) {
83460
- return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
83461
- }
83462
- await driver.relaunch();
83463
- if (resetPolicy === "CLEAR_APP_DATA") {
83464
- log2(" CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
83465
- return {
83466
- appDataCleared: false,
83467
- coldResetUnavailable: true,
83468
- refuseGenerate: true,
83469
- reason: "clear_app_data_unavailable"
83470
- };
83471
- }
83472
- log2(" app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
83473
- return applyScenarioEntryDeepLink(driver, target, {
83474
- appDataCleared: false,
83475
- coldResetUnavailable: true,
83476
- refuseGenerate: false,
83477
- reason: platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
83478
- }, log2);
83532
+ const appDataCleared = await driver.resetAppState?.().catch((err) => {
83533
+ log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
83534
+ return false;
83535
+ }) ?? false;
83536
+ if (appDataCleared) {
83537
+ return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
83479
83538
  }
83480
83539
  await driver.relaunch();
83481
83540
  if (resetPolicy === "CLEAR_APP_DATA") {
83482
- log2(" CLEAR_APP_DATA required but iOS has no supported clearApp primitive \u2014 refusing to generate from warm state.");
83541
+ log2(platform3 === "IOS" ? " CLEAR_APP_DATA required but this iOS device cannot be cleared (simulator required) \u2014 refusing to generate from warm state." : " CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
83483
83542
  return {
83484
83543
  appDataCleared: false,
83485
83544
  coldResetUnavailable: true,
83486
83545
  refuseGenerate: true,
83487
- reason: "ios_clear_app_data_unavailable"
83546
+ reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
83488
83547
  };
83489
83548
  }
83490
- log2(" iOS has no clearApp \u2014 scenario records after relaunch (warm-state risk for auth flows).");
83549
+ log2(platform3 === "IOS" ? " iOS app-data reset unavailable (physical device or simctl failure) \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay)." : " app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
83491
83550
  return applyScenarioEntryDeepLink(driver, target, {
83492
83551
  appDataCleared: false,
83493
83552
  coldResetUnavailable: true,
83494
83553
  refuseGenerate: false,
83495
- reason: "ios_no_clear_app"
83554
+ reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
83496
83555
  }, log2);
83497
83556
  }
83498
83557
  async function runMobileGeneratePhase(args) {
@@ -88953,7 +89012,7 @@ var require_kill_tree = __commonJS({
88953
89012
  var node_child_process_1 = __require("child_process");
88954
89013
  var node_util_1 = __require("util");
88955
89014
  var node_fs_1 = __require("fs");
88956
- var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
89015
+ var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
88957
89016
  var PS_TIMEOUT_MS = 2e3;
88958
89017
  var PS_BUFFER = 1024 * 1024;
88959
89018
  async function gracefulCloseWithTreeKill(options) {
@@ -89007,7 +89066,7 @@ var require_kill_tree = __commonJS({
89007
89066
  timer.unref?.();
89008
89067
  let stdout;
89009
89068
  try {
89010
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
89069
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
89011
89070
  maxBuffer: PS_BUFFER,
89012
89071
  signal: controller.signal
89013
89072
  });
@@ -89045,7 +89104,7 @@ var require_kill_tree = __commonJS({
89045
89104
  timer.unref?.();
89046
89105
  let stdout;
89047
89106
  try {
89048
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
89107
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
89049
89108
  maxBuffer: PS_BUFFER,
89050
89109
  signal: controller.signal
89051
89110
  });
@@ -89122,7 +89181,7 @@ var require_kill_tree = __commonJS({
89122
89181
  timer.unref?.();
89123
89182
  let stdout;
89124
89183
  try {
89125
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
89184
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
89126
89185
  maxBuffer: PS_BUFFER,
89127
89186
  signal: controller.signal
89128
89187
  });
@@ -92288,6 +92347,15 @@ var require_agentic_discovery_prompts = __commonJS({
92288
92347
  lines.push(input.appBrief.trim());
92289
92348
  lines.push("");
92290
92349
  }
92350
+ if (input.knownFactsBlock && input.knownFactsBlock.trim()) {
92351
+ lines.push("## Known facts about this feature (from project documentation)");
92352
+ lines.push("");
92353
+ lines.push("Advisory hints about routes/data/constraints \u2014 verify against the live");
92354
+ lines.push("app; the browser is the source of truth for locators.");
92355
+ lines.push("");
92356
+ lines.push(input.knownFactsBlock.trim());
92357
+ lines.push("");
92358
+ }
92291
92359
  if (input.credentials && input.credentials.length > 0) {
92292
92360
  lines.push("## Test credentials (throwaway accounts)");
92293
92361
  lines.push("");
@@ -98663,8 +98731,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
98663
98731
  onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
98664
98732
  });
98665
98733
  } catch (aiErr) {
98666
- const errMsg2 = aiErr instanceof Error ? aiErr.message : String(aiErr);
98667
- logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg2}`);
98734
+ const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
98735
+ logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
98668
98736
  throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
98669
98737
  }
98670
98738
  const iterDuration = Date.now() - iterStart;
@@ -98707,8 +98775,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
98707
98775
  try {
98708
98776
  toolArgs = JSON.parse(toolCall.function.arguments);
98709
98777
  } catch (parseErr) {
98710
- const errMsg2 = `Invalid tool arguments: ${parseErr.message}`;
98711
- toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg2 });
98778
+ const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
98779
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
98712
98780
  continue;
98713
98781
  }
98714
98782
  if (toolName === "finish_generate") {
@@ -99926,7 +99994,7 @@ var require_util4 = __commonJS({
99926
99994
  }
99927
99995
  path = url.path;
99928
99996
  }
99929
- var isAbsolute = exports.isAbsolute(path);
99997
+ var isAbsolute2 = exports.isAbsolute(path);
99930
99998
  var parts = path.split(/\/+/);
99931
99999
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
99932
100000
  part = parts[i];
@@ -99946,7 +100014,7 @@ var require_util4 = __commonJS({
99946
100014
  }
99947
100015
  path = parts.join("/");
99948
100016
  if (path === "") {
99949
- path = isAbsolute ? "/" : ".";
100017
+ path = isAbsolute2 ? "/" : ".";
99950
100018
  }
99951
100019
  if (url) {
99952
100020
  url.path = path;
@@ -99955,7 +100023,7 @@ var require_util4 = __commonJS({
99955
100023
  return path;
99956
100024
  }
99957
100025
  exports.normalize = normalize;
99958
- function join7(aRoot, aPath) {
100026
+ function join8(aRoot, aPath) {
99959
100027
  if (aRoot === "") {
99960
100028
  aRoot = ".";
99961
100029
  }
@@ -99987,7 +100055,7 @@ var require_util4 = __commonJS({
99987
100055
  }
99988
100056
  return joined;
99989
100057
  }
99990
- exports.join = join7;
100058
+ exports.join = join8;
99991
100059
  exports.isAbsolute = function(aPath) {
99992
100060
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
99993
100061
  };
@@ -100160,7 +100228,7 @@ var require_util4 = __commonJS({
100160
100228
  parsed.path = parsed.path.substring(0, index + 1);
100161
100229
  }
100162
100230
  }
100163
- sourceURL = join7(urlGenerate(parsed), sourceURL);
100231
+ sourceURL = join8(urlGenerate(parsed), sourceURL);
100164
100232
  }
100165
100233
  return normalize(sourceURL);
100166
100234
  }
@@ -313872,8 +313940,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
313872
313940
  ...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
313873
313941
  })), { label: `script-heal-${attempt}` });
313874
313942
  } catch (aiErr) {
313875
- const errMsg2 = aiErr instanceof Error ? aiErr.message : String(aiErr);
313876
- logs2.push(`\u26A0 AI call failed: ${errMsg2}`);
313943
+ const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
313944
+ logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
313877
313945
  attemptHistory.push({
313878
313946
  phase: "script",
313879
313947
  attempt,
@@ -313881,13 +313949,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
313881
313949
  passed: false,
313882
313950
  changedCode: false,
313883
313951
  summary: "AI call failed before returning a heal result",
313884
- errorSummary: truncateForPrompt(errMsg2),
313952
+ errorSummary: truncateForPrompt(errMsg3),
313885
313953
  failedStep: summarizeFailedStep(currentStepResults)
313886
313954
  });
313887
313955
  if (feedbackEnabled) {
313888
- lastTransportFailure = truncateForPrompt(errMsg2, 200) ?? "the AI request failed";
313956
+ lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
313889
313957
  } else {
313890
- currentError = `AI call failed on attempt ${attempt}: ${errMsg2}`;
313958
+ currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
313891
313959
  }
313892
313960
  markRecoverableTurn();
313893
313961
  continue;
@@ -314137,8 +314205,8 @@ ${currentError.slice(0, 2e3)}`
314137
314205
  // item 5b
314138
314206
  });
314139
314207
  } catch (runErr) {
314140
- const errMsg2 = runErr instanceof Error ? runErr.message : String(runErr);
314141
- logs2.push(`\u{1F4A5} Native execution threw: ${errMsg2}`);
314208
+ const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
314209
+ logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
314142
314210
  attemptHistory.push({
314143
314211
  phase: "script",
314144
314212
  attempt,
@@ -314146,9 +314214,9 @@ ${currentError.slice(0, 2e3)}`
314146
314214
  passed: false,
314147
314215
  changedCode: true,
314148
314216
  summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
314149
- errorSummary: truncateForPrompt(errMsg2)
314217
+ errorSummary: truncateForPrompt(errMsg3)
314150
314218
  });
314151
- currentError = `Native execution threw: ${errMsg2}`;
314219
+ currentError = `Native execution threw: ${errMsg3}`;
314152
314220
  currentStepResults = [];
314153
314221
  continue;
314154
314222
  }
@@ -314904,9 +314972,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
314904
314972
  previousErrorSignature = errorSig;
314905
314973
  }
314906
314974
  } catch (runErr) {
314907
- const errMsg2 = runErr instanceof Error ? runErr.message : String(runErr);
314908
- logs2.push(`\u{1F4A5} Native run threw: ${errMsg2}`);
314909
- currentError = errMsg2;
314975
+ const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
314976
+ logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
314977
+ currentError = errMsg3;
314910
314978
  currentStepResults = [];
314911
314979
  }
314912
314980
  } else {
@@ -315468,8 +315536,8 @@ When you call finish_fix, preserve locators only when they are stable structural
315468
315536
  }
315469
315537
  }
315470
315538
  } catch (toolErr) {
315471
- const errMsg2 = toolErr instanceof Error ? toolErr.message : String(toolErr);
315472
- let errorContent = `Error: ${errMsg2}`;
315539
+ const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
315540
+ let errorContent = `Error: ${errMsg3}`;
315473
315541
  try {
315474
315542
  const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
315475
315543
  const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
@@ -315490,8 +315558,8 @@ ${errorContent}`;
315490
315558
  }
315491
315559
  }
315492
315560
  } catch (mcpErr) {
315493
- const errMsg2 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
315494
- logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg2}`);
315561
+ const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
315562
+ logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
315495
315563
  }
315496
315564
  if (verdict === "app_bug") {
315497
315565
  phase2AttemptHistory.push({
@@ -316054,9 +316122,9 @@ ${testBlocks}
316054
316122
  }
316055
316123
  emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
316056
316124
  } catch (err) {
316057
- const errMsg2 = err instanceof Error ? err.message : String(err);
316058
- toolResultContent = `ERROR: ${errMsg2}`;
316059
- emit(` \u2717 ${errMsg2}`);
316125
+ const errMsg3 = err instanceof Error ? err.message : String(err);
316126
+ toolResultContent = `ERROR: ${errMsg3}`;
316127
+ emit(` \u2717 ${errMsg3}`);
316060
316128
  }
316061
316129
  toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
316062
316130
  }
@@ -321911,8 +321979,8 @@ var require_capture_page_normalizer = __commonJS({
321911
321979
  const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
321912
321980
  snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
321913
321981
  } catch (err) {
321914
- const errMsg2 = err instanceof Error ? err.message : String(err);
321915
- const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg2);
321982
+ const errMsg3 = err instanceof Error ? err.message : String(err);
321983
+ const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
321916
321984
  return {
321917
321985
  snapshotText: null,
321918
321986
  meta: {
@@ -325934,9 +326002,9 @@ ${currentSummary}`;
325934
326002
  });
325935
326003
  }
325936
326004
  } catch (err) {
325937
- const errMsg2 = err instanceof Error ? err.message : String(err);
325938
- logs2.push(` Error persisting page: ${errMsg2}`);
325939
- toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg2}` });
326005
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326006
+ logs2.push(` Error persisting page: ${errMsg3}`);
326007
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
325940
326008
  }
325941
326009
  continue;
325942
326010
  }
@@ -326138,12 +326206,12 @@ ${currentSummary}`;
326138
326206
  })
326139
326207
  });
326140
326208
  } catch (err) {
326141
- const errMsg2 = err instanceof Error ? err.message : String(err);
326142
- logs2.push(` Error persisting page: ${errMsg2}`);
326209
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326210
+ logs2.push(` Error persisting page: ${errMsg3}`);
326143
326211
  toolResults.push({
326144
326212
  role: "tool",
326145
326213
  tool_call_id: toolCall.id,
326146
- content: `ERROR persisting page: ${errMsg2}`
326214
+ content: `ERROR persisting page: ${errMsg3}`
326147
326215
  });
326148
326216
  }
326149
326217
  continue;
@@ -326178,12 +326246,12 @@ ${currentSummary}`;
326178
326246
  content: JSON.stringify({ acknowledged: true })
326179
326247
  });
326180
326248
  } catch (err) {
326181
- const errMsg2 = err instanceof Error ? err.message : String(err);
326182
- logs2.push(` Error persisting transition: ${errMsg2}`);
326249
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326250
+ logs2.push(` Error persisting transition: ${errMsg3}`);
326183
326251
  toolResults.push({
326184
326252
  role: "tool",
326185
326253
  tool_call_id: toolCall.id,
326186
- content: `ERROR persisting transition: ${errMsg2}`
326254
+ content: `ERROR persisting transition: ${errMsg3}`
326187
326255
  });
326188
326256
  }
326189
326257
  continue;
@@ -326838,15 +326906,15 @@ Exploration complete: ${summary}`);
326838
326906
  toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
326839
326907
  logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
326840
326908
  } catch (err) {
326841
- const errMsg2 = err instanceof Error ? err.message : String(err);
326842
- toolResultContent = `ERROR: ${errMsg2}`;
326843
- logs2.push(` Error: ${errMsg2}`);
326909
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326910
+ toolResultContent = `ERROR: ${errMsg3}`;
326911
+ logs2.push(` Error: ${errMsg3}`);
326844
326912
  if (useV2) {
326845
326913
  (0, exploration_state_js_1.reduceEvent)(state2, {
326846
326914
  type: "ACTION_FAILED",
326847
326915
  route: state2.currentRoute,
326848
326916
  action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
326849
- error: errMsg2
326917
+ error: errMsg3
326850
326918
  });
326851
326919
  }
326852
326920
  }
@@ -328955,6 +329023,15 @@ var require_agentic_survey_prompts = __commonJS({
328955
329023
  lines.push(input.appBrief.trim());
328956
329024
  lines.push("");
328957
329025
  }
329026
+ if (input.knownFactsBlock && input.knownFactsBlock.trim().length > 0) {
329027
+ lines.push("## Known facts about this app (from project documentation)");
329028
+ lines.push("");
329029
+ lines.push("Use these as hints for what features/routes/constraints exist. They are");
329030
+ lines.push("advisory \u2014 verify against the live app; the browser is the source of truth.");
329031
+ lines.push("");
329032
+ lines.push(input.knownFactsBlock.trim());
329033
+ lines.push("");
329034
+ }
328958
329035
  if (input.credentials && input.credentials.length > 0) {
328959
329036
  lines.push("## Test credentials (throwaway accounts)");
328960
329037
  lines.push("");
@@ -329582,6 +329659,7 @@ var require_grok_survey_runner = __commonJS({
329582
329659
  const briefInput = {
329583
329660
  baseUrl: ctx.baseUrl,
329584
329661
  appBrief: ctx.appBrief,
329662
+ knownFactsBlock: ctx.knowledgeContext,
329585
329663
  credentials: credentials.length > 0 ? credentials : void 0,
329586
329664
  existingFeatureNames: ctx.existingFeatureNames ?? [],
329587
329665
  focusAreas: ctx.focusAreas,
@@ -329682,10 +329760,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
329682
329760
  onLog: options.onLog,
329683
329761
  onProvenCommands: recordProvenCommands
329684
329762
  });
329685
- const errMsg2 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
329763
+ const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
329686
329764
  const fixPrompt = [
329687
329765
  "Your previous attempt did not produce a valid result file.",
329688
- `Error: ${errMsg2}`,
329766
+ `Error: ${errMsg3}`,
329689
329767
  `Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
329690
329768
  "Single JSON object. No prose. Then exit."
329691
329769
  ].join("\n");
@@ -331277,6 +331355,7 @@ var require_grok_discovery_runner = __commonJS({
331277
331355
  featureObservations: ctx.featureContext?.flowIntel,
331278
331356
  notesForTestAuthor: ctx.featureContext?.priorInsight,
331279
331357
  appBrief: ctx.appBrief,
331358
+ knownFactsBlock: ctx.knowledgeContext,
331280
331359
  credentials: credentials.length > 0 ? credentials : void 0,
331281
331360
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
331282
331361
  existingTestNames: ctx.existingTestNames ?? [],
@@ -331825,7 +331904,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
331825
331904
  log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
331826
331905
  }
331827
331906
  if (!parsedResult) {
331828
- const errMsg2 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
331907
+ const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
331829
331908
  if (planDebounceTimerRef.current) {
331830
331909
  clearTimeout(planDebounceTimerRef.current);
331831
331910
  planDebounceTimerRef.current = void 0;
@@ -331850,7 +331929,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
331850
331929
  ""
331851
331930
  ].join("\n");
331852
331931
  const fixPrompt = [
331853
- `Your previous attempt did not produce a valid result file. Error: ${errMsg2}`,
331932
+ `Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
331854
331933
  "Re-read BRIEF.md.",
331855
331934
  phaseContext,
331856
331935
  `Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
@@ -332214,6 +332293,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
332214
332293
  featureObservations: ctx.featureContext?.flowIntel,
332215
332294
  notesForTestAuthor: ctx.featureContext?.priorInsight,
332216
332295
  appBrief: ctx.appBrief,
332296
+ knownFactsBlock: ctx.knowledgeContext,
332217
332297
  credentials: credentials.length > 0 ? credentials : void 0,
332218
332298
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
332219
332299
  existingTestNames: ctx.existingTestNames ?? [],
@@ -333305,6 +333385,7 @@ ${preview}`);
333305
333385
  const briefInput = {
333306
333386
  baseUrl: ctx.baseUrl,
333307
333387
  appBrief: ctx.appBrief,
333388
+ knownFactsBlock: ctx.knowledgeContext,
333308
333389
  credentials: credentials.length > 0 ? credentials : void 0,
333309
333390
  existingFeatureNames: ctx.existingFeatureNames ?? [],
333310
333391
  focusAreas: ctx.focusAreas,
@@ -333477,10 +333558,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
333477
333558
  onLog: parserLog,
333478
333559
  onProvenCommands: recordProvenCommands
333479
333560
  });
333480
- const errMsg2 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
333561
+ const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
333481
333562
  const fixPrompt = [
333482
333563
  "Your previous attempt did not leave a valid result file.",
333483
- `Error: ${errMsg2}`,
333564
+ `Error: ${errMsg3}`,
333484
333565
  `Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
333485
333566
  "Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
333486
333567
  `the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
@@ -334164,6 +334245,7 @@ var require_claude_discovery_runner = __commonJS({
334164
334245
  featureObservations: ctx.featureContext?.flowIntel,
334165
334246
  notesForTestAuthor: ctx.featureContext?.priorInsight,
334166
334247
  appBrief: ctx.appBrief,
334248
+ knownFactsBlock: ctx.knowledgeContext,
334167
334249
  credentials: credentials.length > 0 ? credentials : void 0,
334168
334250
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
334169
334251
  existingTestNames: ctx.existingTestNames ?? [],
@@ -336018,7 +336100,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
336018
336100
  // scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
336019
336101
  // auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
336020
336102
  emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
336021
- testBudget
336103
+ testBudget,
336104
+ knowledgeContext: ctx.knowledgeContext
336022
336105
  }, {
336023
336106
  modelOverride: ctx.planModel ?? ctx.generateModel,
336024
336107
  gateModelOverride: ctx.planGateModel,
@@ -336130,6 +336213,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
336130
336213
  interactionLog: exploreResult.interactionLog,
336131
336214
  modelOverride: ctx.generateModel,
336132
336215
  appBrief: ctx.appBrief,
336216
+ knowledgeContext: ctx.knowledgeContext,
336133
336217
  onAICall,
336134
336218
  onProgress,
336135
336219
  mcpClient: mcpClient ?? void 0,
@@ -336609,7 +336693,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
336609
336693
  // configured inbox), so the planner never promises what generation can't do.
336610
336694
  // Scoped to AUTH features: a non-auth feature has no signup surface to test.
336611
336695
  emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
336612
- testBudget: featureTestBudget
336696
+ testBudget: featureTestBudget,
336697
+ knowledgeContext: ctx.knowledgeContext
336613
336698
  }, {
336614
336699
  modelOverride: ctx.planModel ?? ctx.generateModel,
336615
336700
  gateModelOverride: ctx.planGateModel,
@@ -336655,6 +336740,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
336655
336740
  interactionLog: exploreResult.interactionLog,
336656
336741
  modelOverride: ctx.generateModel,
336657
336742
  appBrief: ctx.appBrief,
336743
+ knowledgeContext: ctx.knowledgeContext,
336658
336744
  onAICall,
336659
336745
  mcpClient: mcpClient ?? void 0,
336660
336746
  // Pooled browser for parallel scenario generation (Fargate opt-in via
@@ -339853,8 +339939,8 @@ var require_adm_zip = __commonJS({
339853
339939
  return null;
339854
339940
  }
339855
339941
  function fixPath(zipPath) {
339856
- const { join: join7, normalize, sep } = pth.posix;
339857
- return join7(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
339942
+ const { join: join8, normalize, sep } = pth.posix;
339943
+ return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
339858
339944
  }
339859
339945
  function filenameFilter(filterfn) {
339860
339946
  if (filterfn instanceof RegExp) {
@@ -342689,11 +342775,11 @@ var require_winchars = __commonJS({
342689
342775
  var require_strip_absolute_path = __commonJS({
342690
342776
  "../../node_modules/tar/lib/strip-absolute-path.js"(exports, module) {
342691
342777
  "use strict";
342692
- var { isAbsolute, parse } = __require("path").win32;
342778
+ var { isAbsolute: isAbsolute2, parse } = __require("path").win32;
342693
342779
  module.exports = (path) => {
342694
342780
  let r = "";
342695
342781
  let parsed = parse(path);
342696
- while (isAbsolute(path) || parsed.root) {
342782
+ while (isAbsolute2(path) || parsed.root) {
342697
342783
  const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
342698
342784
  path = path.slice(root.length);
342699
342785
  r += root;
@@ -345205,7 +345291,7 @@ var require_update = __commonJS({
345205
345291
  var require_opts_arg = __commonJS({
345206
345292
  "../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
345207
345293
  "use strict";
345208
- var { promisify: promisify3 } = __require("util");
345294
+ var { promisify: promisify4 } = __require("util");
345209
345295
  var fs = __require("fs");
345210
345296
  var optsArg = (opts) => {
345211
345297
  if (!opts)
@@ -345219,9 +345305,9 @@ var require_opts_arg = __commonJS({
345219
345305
  else
345220
345306
  throw new TypeError("invalid options argument");
345221
345307
  opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
345222
- opts.mkdirAsync = promisify3(opts.mkdir);
345308
+ opts.mkdirAsync = promisify4(opts.mkdir);
345223
345309
  opts.stat = opts.stat || opts.fs.stat || fs.stat;
345224
- opts.statAsync = promisify3(opts.stat);
345310
+ opts.statAsync = promisify4(opts.stat);
345225
345311
  opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
345226
345312
  opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
345227
345313
  return opts;
@@ -345474,10 +345560,10 @@ var require_chownr = __commonJS({
345474
345560
  }
345475
345561
  } : (path2, uid, gid) => lchownSync(path2, uid, gid);
345476
345562
  var nodeVersion = process.version;
345477
- var readdir2 = (path2, options, cb) => fs.readdir(path2, options, cb);
345563
+ var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
345478
345564
  var readdirSync = (path2, options) => fs.readdirSync(path2, options);
345479
345565
  if (/^v4\./.test(nodeVersion))
345480
- readdir2 = (path2, options, cb) => fs.readdir(path2, cb);
345566
+ readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
345481
345567
  var chown = (cpath, uid, gid, cb) => {
345482
345568
  fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
345483
345569
  cb(er && er.code !== "ENOENT" ? er : null);
@@ -345504,7 +345590,7 @@ var require_chownr = __commonJS({
345504
345590
  }
345505
345591
  };
345506
345592
  var chownr = (p, uid, gid, cb) => {
345507
- readdir2(p, { withFileTypes: true }, (er, children) => {
345593
+ readdir3(p, { withFileTypes: true }, (er, children) => {
345508
345594
  if (er) {
345509
345595
  if (er.code === "ENOENT")
345510
345596
  return cb();
@@ -345778,7 +345864,7 @@ var require_path_reservations = __commonJS({
345778
345864
  var assert = __require("assert");
345779
345865
  var normalize = require_normalize_unicode();
345780
345866
  var stripSlashes = require_strip_trailing_slashes();
345781
- var { join: join7 } = __require("path");
345867
+ var { join: join8 } = __require("path");
345782
345868
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
345783
345869
  var isWindows = platform3 === "win32";
345784
345870
  module.exports = () => {
@@ -345787,7 +345873,7 @@ var require_path_reservations = __commonJS({
345787
345873
  const getDirs = (path) => {
345788
345874
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
345789
345875
  if (set.length) {
345790
- path2 = join7(set[set.length - 1], path2);
345876
+ path2 = join8(set[set.length - 1], path2);
345791
345877
  }
345792
345878
  set.push(path2 || "/");
345793
345879
  return set;
@@ -345855,7 +345941,7 @@ var require_path_reservations = __commonJS({
345855
345941
  };
345856
345942
  const reserve = (paths, fn) => {
345857
345943
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
345858
- return stripSlashes(join7(normalize(p))).toLowerCase();
345944
+ return stripSlashes(join8(normalize(p))).toLowerCase();
345859
345945
  });
345860
345946
  const dirs = new Set(
345861
345947
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -347868,6 +347954,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347868
347954
  "Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
347869
347955
  "assertVisible or assertText unless tagging @needs-review. When an incremental",
347870
347956
  "allow-list is present, extract ONLY from those files.",
347957
+ "",
347958
+ "Every tap/type/clear/waitForElement/assertVisible/assertText target MUST",
347959
+ "carry an IDENTITY locator: ~accessibility-id, id=resource-id, visible text,",
347960
+ "or an attribute-EQUALITY xpath (//android.widget.Button[@text='Submit']).",
347961
+ "Positional/index-only xpaths (/hierarchy/\u2026/View[3]) are rejected \u2014 imported",
347962
+ "steps have no recorded coordinates, so such a step can never replay.",
347871
347963
  "Write strictly valid JSON."
347872
347964
  ].join("\n");
347873
347965
  }
@@ -347885,6 +347977,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347885
347977
  function isPlainObject2(value) {
347886
347978
  return !!value && typeof value === "object" && !Array.isArray(value);
347887
347979
  }
347980
+ var LOCATOR_REQUIRING_IMPORT_ACTIONS = /* @__PURE__ */ new Set([
347981
+ "tap",
347982
+ "type",
347983
+ "clear",
347984
+ "waitForElement",
347985
+ "assertVisible",
347986
+ "assertText"
347987
+ ]);
347988
+ function selectorValueHasIdentity(value) {
347989
+ const trimmed = value.trim();
347990
+ if (!trimmed)
347991
+ return false;
347992
+ if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
347993
+ return (0, shared_1.isIdentityMobileXpath)(trimmed);
347994
+ return true;
347995
+ }
347996
+ function stepLocatorLacksIdentity(step) {
347997
+ if (!LOCATOR_REQUIRING_IMPORT_ACTIONS.has(step.action))
347998
+ return false;
347999
+ const target = step.target;
348000
+ if (!target)
348001
+ return true;
348002
+ if ((0, shared_1.mobileLocatorBundleHasIdentity)(target.locatorBundle))
348003
+ return false;
348004
+ return ![target.primary, ...target.fallbacks ?? []].some((value) => typeof value === "string" && selectorValueHasIdentity(value));
348005
+ }
347888
348006
  function isValidStep(value) {
347889
348007
  if (!value || typeof value !== "object" || Array.isArray(value))
347890
348008
  return false;
@@ -347936,6 +348054,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347936
348054
  if (!o.steps.every(isValidStep)) {
347937
348055
  return { ok: false, reason: "steps: invalid step shape" };
347938
348056
  }
348057
+ const identityless = o.steps.find(stepLocatorLacksIdentity);
348058
+ if (identityless) {
348059
+ return {
348060
+ ok: false,
348061
+ reason: `steps: step ${identityless.order} (${identityless.action}) has no identity locator \u2014 a positional/index xpath cannot replay (imported steps have no recorded coordinates). Use ~accessibility-id, id=resource-id, visible text, or an attribute-equality xpath like //android.widget.Button[@text='Submit'].`
348062
+ };
348063
+ }
347939
348064
  const steps = o.steps.map((step, order) => ({
347940
348065
  ...step,
347941
348066
  order,
@@ -350031,7 +350156,7 @@ var require_process_inventory = __commonJS({
350031
350156
  var node_child_process_1 = __require("child_process");
350032
350157
  var node_os_1 = __importDefault(__require("os"));
350033
350158
  var node_util_1 = __require("util");
350034
- var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
350159
+ var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
350035
350160
  var DEFAULT_LIMIT = 60;
350036
350161
  var DEFAULT_TIMEOUT_MS2 = 800;
350037
350162
  var MAX_COMMAND_LENGTH = 320;
@@ -350093,7 +350218,7 @@ var require_process_inventory = __commonJS({
350093
350218
  const timer = setTimeout(() => controller.abort(), timeoutMs);
350094
350219
  timer.unref?.();
350095
350220
  try {
350096
- const { stdout } = await execFileAsync3(command, args, {
350221
+ const { stdout } = await execFileAsync4(command, args, {
350097
350222
  maxBuffer: 1024 * 1024,
350098
350223
  signal: controller.signal
350099
350224
  });
@@ -351229,7 +351354,7 @@ var require_package4 = __commonJS({
351229
351354
  "package.json"(exports, module) {
351230
351355
  module.exports = {
351231
351356
  name: "@validate.qa/runner",
351232
- version: "1.0.67",
351357
+ version: "1.0.69",
351233
351358
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
351234
351359
  bin: {
351235
351360
  "validate-runner": "dist/cli.js"
@@ -351316,7 +351441,7 @@ function getConfigDir() {
351316
351441
  }
351317
351442
 
351318
351443
  // src/runner.ts
351319
- import { join as join6 } from "path";
351444
+ import { join as join7 } from "path";
351320
351445
  import { homedir as homedir3 } from "os";
351321
351446
  import { createHash } from "crypto";
351322
351447
 
@@ -351911,6 +352036,66 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
351911
352036
  };
351912
352037
  }
351913
352038
 
352039
+ // src/services/mobile/ios-simulator-reset.ts
352040
+ import { execFile } from "child_process";
352041
+ import { promisify } from "util";
352042
+ import { readdir, rm } from "fs/promises";
352043
+ import { join as join3, isAbsolute } from "path";
352044
+ var execFileAsync = promisify(execFile);
352045
+ var SIMCTL_TIMEOUT_MS = 15e3;
352046
+ var SIMULATOR_UDID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
352047
+ function isIosSimulatorUdid(udid) {
352048
+ return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
352049
+ }
352050
+ function errMsg(err) {
352051
+ return err instanceof Error ? err.message : String(err);
352052
+ }
352053
+ async function simctl(args) {
352054
+ const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
352055
+ timeout: SIMCTL_TIMEOUT_MS,
352056
+ encoding: "utf-8"
352057
+ });
352058
+ return stdout.trim();
352059
+ }
352060
+ async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
352061
+ if (!isIosSimulatorUdid(udid)) {
352062
+ return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
352063
+ }
352064
+ let container;
352065
+ try {
352066
+ container = await simctl(["get_app_container", udid, bundleId, "data"]);
352067
+ } catch (err) {
352068
+ return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
352069
+ }
352070
+ if (!isAbsolute(container) || !container.includes("/Containers/Data/Application/")) {
352071
+ return { ok: false, detail: `unexpected app container path: ${container}` };
352072
+ }
352073
+ try {
352074
+ await simctl(["spawn", udid, "defaults", "delete", bundleId]);
352075
+ } catch {
352076
+ }
352077
+ try {
352078
+ const entries = await readdir(container);
352079
+ for (const entry of entries) {
352080
+ await rm(join3(container, entry), { recursive: true, force: true });
352081
+ }
352082
+ } catch (err) {
352083
+ return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
352084
+ }
352085
+ const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
352086
+ if (keychainPolicy !== "never") {
352087
+ try {
352088
+ await simctl(["keychain", udid, "reset"]);
352089
+ } catch (err) {
352090
+ return {
352091
+ ok: false,
352092
+ detail: `simulator keychain reset failed (${errMsg(err)}) \u2014 app data was wiped but keychain items (e.g. auth tokens) may survive. Set VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET=never to skip the keychain step.`
352093
+ };
352094
+ }
352095
+ }
352096
+ return { ok: true };
352097
+ }
352098
+
351914
352099
  // src/services/mobile/appium-actions.ts
351915
352100
  var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
351916
352101
  var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
@@ -352093,9 +352278,12 @@ async function tapAt(sessionPath, bounds) {
352093
352278
  }
352094
352279
  }
352095
352280
  function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
352096
- if (platform3 !== "ANDROID") return false;
352097
352281
  const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
352098
352282
  if (policy === "never") return false;
352283
+ if (platform3 === "IOS") {
352284
+ return isIosSimulatorUdid(deviceId);
352285
+ }
352286
+ if (platform3 !== "ANDROID") return false;
352099
352287
  if (policy === "always") return true;
352100
352288
  return !!deviceId && deviceId.startsWith("emulator-");
352101
352289
  }
@@ -352976,10 +353164,10 @@ async function pressHome(sessionPath, platform3) {
352976
353164
 
352977
353165
  // src/services/appium-executor.ts
352978
353166
  var import_runner_core8 = __toESM(require_dist2());
352979
- import { execFile as execFile2 } from "child_process";
352980
- import { promisify as promisify2 } from "util";
353167
+ import { execFile as execFile3 } from "child_process";
353168
+ import { promisify as promisify3 } from "util";
352981
353169
  import { tmpdir as tmpdir3 } from "os";
352982
- import { join as join4 } from "path";
353170
+ import { join as join5 } from "path";
352983
353171
 
352984
353172
  // src/services/device-discovery.ts
352985
353173
  import { execFileSync as execFileSync2 } from "child_process";
@@ -353286,9 +353474,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
353286
353474
  function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
353287
353475
  const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
353288
353476
  if (ready.length === 0) return { kind: "busy", reason: `no booted ${platform3} device` };
353289
- if (requirements.resetPolicy === "CLEAR_APP_DATA" && platform3 !== "ANDROID") {
353290
- return { kind: "unsatisfiable", reason: "CLEAR_APP_DATA is unsupported on iOS" };
353291
- }
353292
353477
  let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
353293
353478
  const appId = requirements.appId?.trim();
353294
353479
  if (appId) {
@@ -353337,23 +353522,23 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
353337
353522
 
353338
353523
  // src/services/mobile/mobile-network-capture.ts
353339
353524
  var import_runner_core7 = __toESM(require_dist2());
353340
- import { execFile, execFileSync as execFileSync3 } from "child_process";
353525
+ import { execFile as execFile2, execFileSync as execFileSync3 } from "child_process";
353341
353526
  import { randomUUID } from "crypto";
353342
- import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
353527
+ import { mkdtemp, rm as rm2, writeFile, readFile, readdir as readdir2, mkdir, unlink } from "fs/promises";
353343
353528
  import { tmpdir as tmpdir2, networkInterfaces } from "os";
353344
- import { join as join3 } from "path";
353345
- import { promisify } from "util";
353529
+ import { join as join4 } from "path";
353530
+ import { promisify as promisify2 } from "util";
353346
353531
  import {
353347
353532
  getLocal,
353348
353533
  generateCACertificate
353349
353534
  } from "mockttp";
353350
- var execFileAsync = promisify(execFile);
353535
+ var execFileAsync2 = promisify2(execFile2);
353351
353536
  var MAX_CALLS = 1e3;
353352
353537
  var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
353353
353538
  var DEVICE_CMD_TIMEOUT_MS = 15e3;
353354
353539
  var CA_KEY_BITS = 2048;
353355
353540
  var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
353356
- var PENDING_CLEANUP_DIR = join3(tmpdir2(), "validateqa-mobile-capture-pending");
353541
+ var PENDING_CLEANUP_DIR = join4(tmpdir2(), "validateqa-mobile-capture-pending");
353357
353542
  var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
353358
353543
  var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
353359
353544
  function envFlag(name) {
@@ -353535,7 +353720,7 @@ var MitmProxy = class {
353535
353720
  try {
353536
353721
  await server3.stop();
353537
353722
  } catch (err) {
353538
- this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg(err)}`);
353723
+ this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
353539
353724
  }
353540
353725
  this.onLog?.(
353541
353726
  `[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
@@ -353560,7 +353745,7 @@ var MitmProxy = class {
353560
353745
  isHttps
353561
353746
  });
353562
353747
  } catch (err) {
353563
- this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg(err)}`);
353748
+ this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
353564
353749
  }
353565
353750
  }
353566
353751
  // ── response → finalize the matched request ───────────────────────────────
@@ -353603,7 +353788,7 @@ var MitmProxy = class {
353603
353788
  intercepted: true
353604
353789
  });
353605
353790
  } catch (err) {
353606
- this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg(err)}`);
353791
+ this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
353607
353792
  }
353608
353793
  }
353609
353794
  // ── tls-client-error → record WHY interception failed ─────────────────────
@@ -353680,17 +353865,17 @@ async function generateCaCert(dir, onLog) {
353680
353865
  // rely on stop() to remove the cert from the device promptly after the run
353681
353866
  // rather than on a short expiry.
353682
353867
  });
353683
- const caCertPath = join3(dir, "validateqa-mobile-ca.pem");
353868
+ const caCertPath = join4(dir, "validateqa-mobile-ca.pem");
353684
353869
  await writeFile(caCertPath, cert, "utf-8");
353685
353870
  return { caCertPath, caKeyPem: key, caCertPem: cert };
353686
353871
  } catch (err) {
353687
- onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg(err)}`);
353872
+ onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
353688
353873
  return null;
353689
353874
  }
353690
353875
  }
353691
353876
  async function androidReadProxy(deviceId) {
353692
353877
  try {
353693
- const { stdout } = await execFileAsync(
353878
+ const { stdout } = await execFileAsync2(
353694
353879
  "adb",
353695
353880
  ["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
353696
353881
  { timeout: DEVICE_CMD_TIMEOUT_MS }
@@ -353703,33 +353888,33 @@ async function androidReadProxy(deviceId) {
353703
353888
  }
353704
353889
  async function androidSetProxy(deviceId, hostPort, onLog) {
353705
353890
  try {
353706
- await execFileAsync(
353891
+ await execFileAsync2(
353707
353892
  "adb",
353708
353893
  ["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
353709
353894
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353710
353895
  );
353711
353896
  return true;
353712
353897
  } catch (err) {
353713
- onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg(err)}`);
353898
+ onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
353714
353899
  return false;
353715
353900
  }
353716
353901
  }
353717
353902
  async function androidClearProxy(deviceId, originalProxy, onLog) {
353718
353903
  const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
353719
353904
  try {
353720
- await execFileAsync(
353905
+ await execFileAsync2(
353721
353906
  "adb",
353722
353907
  ["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
353723
353908
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353724
353909
  );
353725
353910
  } catch (err) {
353726
- onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg(err)}`);
353911
+ onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
353727
353912
  }
353728
353913
  }
353729
353914
  async function androidInstallCa(deviceId, caCertPath) {
353730
353915
  const devicePath = "/sdcard/validateqa-mobile-ca.pem";
353731
353916
  try {
353732
- await execFileAsync("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
353917
+ await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
353733
353918
  timeout: DEVICE_CMD_TIMEOUT_MS
353734
353919
  });
353735
353920
  return {
@@ -353737,23 +353922,23 @@ async function androidInstallCa(deviceId, caCertPath) {
353737
353922
  note: `CA copied from ${caCertPath} to ${devicePath} but NOT auto-trusted: Android 7+ apps must opt into user CAs (network_security_config), and the system store requires a rooted/writable image. HTTPS bodies decrypt only for apps that trust user CAs; pinned apps never decrypt.`
353738
353923
  };
353739
353924
  } catch (err) {
353740
- return { installed: false, note: `CA push failed: ${errMsg(err)}` };
353925
+ return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
353741
353926
  }
353742
353927
  }
353743
353928
  async function androidRemoveCa(deviceId, onLog) {
353744
353929
  try {
353745
- await execFileAsync(
353930
+ await execFileAsync2(
353746
353931
  "adb",
353747
353932
  ["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
353748
353933
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353749
353934
  );
353750
353935
  } catch (err) {
353751
- onLog?.(`[mobile-net] adb remove CA failed: ${errMsg(err)}`);
353936
+ onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
353752
353937
  }
353753
353938
  }
353754
353939
  async function iosSimTrustCa(deviceId, caCertPath) {
353755
353940
  try {
353756
- await execFileAsync("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
353941
+ await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
353757
353942
  timeout: DEVICE_CMD_TIMEOUT_MS
353758
353943
  });
353759
353944
  return {
@@ -353761,18 +353946,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
353761
353946
  note: "CA added to simulator keychain (add-root-cert). NOTE: the simulator has no per-app HTTP proxy setting controllable via simctl \u2014 it shares the host network, so only traffic that actually routes through the proxy is intercepted; pinned apps still never decrypt."
353762
353947
  };
353763
353948
  } catch (err) {
353764
- return { trusted: false, note: `simctl add-root-cert failed: ${errMsg(err)}` };
353949
+ return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
353765
353950
  }
353766
353951
  }
353767
353952
  async function iosSimRemoveCa(deviceId, onLog) {
353768
353953
  if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
353769
353954
  try {
353770
- await execFileAsync("xcrun", ["simctl", "keychain", deviceId, "reset"], {
353955
+ await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
353771
353956
  timeout: DEVICE_CMD_TIMEOUT_MS
353772
353957
  });
353773
353958
  onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
353774
353959
  } catch (err) {
353775
- onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg(err)}`);
353960
+ onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
353776
353961
  }
353777
353962
  return;
353778
353963
  }
@@ -353780,7 +353965,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
353780
353965
  `[mobile-net] NOTE: the validate.qa capture CA remains TRUSTED in simulator ${deviceId}. simctl has no scoped remove (only a full keychain reset that wipes ALL certs), so we leave the single CA in place. Use a throwaway simulator, or set VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET=1 to reset the whole sim keychain on teardown.`
353781
353966
  );
353782
353967
  }
353783
- function errMsg(err) {
353968
+ function errMsg2(err) {
353784
353969
  return err instanceof Error ? err.message : String(err);
353785
353970
  }
353786
353971
  function isProcessAlive(pid) {
@@ -353871,7 +354056,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
353871
354056
  let service;
353872
354057
  let prior;
353873
354058
  try {
353874
- const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
354059
+ const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
353875
354060
  timeout: DEVICE_CMD_TIMEOUT_MS
353876
354061
  });
353877
354062
  const svc = parsePrimaryNetworkService(order);
@@ -353880,26 +354065,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
353880
354065
  }
353881
354066
  service = svc;
353882
354067
  const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
353883
- execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
353884
- execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
354068
+ execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
354069
+ execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
353885
354070
  ]);
353886
354071
  prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
353887
354072
  for (const args of buildHostProxySetCommands(service, host, port)) {
353888
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354073
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353889
354074
  }
353890
354075
  onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
353891
354076
  return { configured: true, service, prior };
353892
354077
  } catch (err) {
353893
354078
  if (service && prior) {
353894
- onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg(err)}`);
354079
+ onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
353895
354080
  for (const args of buildHostProxyRestoreCommands(service, prior)) {
353896
354081
  try {
353897
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354082
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353898
354083
  } catch {
353899
354084
  }
353900
354085
  }
353901
354086
  }
353902
- return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
354087
+ return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
353903
354088
  }
353904
354089
  }
353905
354090
  async function iosSimRestoreHostProxy(service, prior, onLog) {
@@ -353909,9 +354094,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
353909
354094
  };
353910
354095
  for (const args of buildHostProxyRestoreCommands(service, restore)) {
353911
354096
  try {
353912
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354097
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353913
354098
  } catch (err) {
353914
- onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg(err)}`);
354099
+ onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
353915
354100
  }
353916
354101
  }
353917
354102
  onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
@@ -353919,7 +354104,7 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
353919
354104
  var activeCleanups = /* @__PURE__ */ new Map();
353920
354105
  var exitHandlersInstalled = false;
353921
354106
  function markerPath(sessionKey) {
353922
- return join3(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
354107
+ return join4(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
353923
354108
  }
353924
354109
  async function writePendingCleanup(sessionKey, marker) {
353925
354110
  try {
@@ -353963,7 +354148,7 @@ function restoreDeviceSync(marker, onLog) {
353963
354148
  async function reconcilePendingCleanups(onLog) {
353964
354149
  let files;
353965
354150
  try {
353966
- files = await readdir(PENDING_CLEANUP_DIR);
354151
+ files = await readdir2(PENDING_CLEANUP_DIR);
353967
354152
  } catch {
353968
354153
  return;
353969
354154
  }
@@ -353971,7 +354156,7 @@ async function reconcilePendingCleanups(onLog) {
353971
354156
  if (!file.endsWith(".json")) continue;
353972
354157
  const sessionKey = file.slice(0, -".json".length);
353973
354158
  if (activeCleanups.has(sessionKey)) continue;
353974
- const full = join3(PENDING_CLEANUP_DIR, file);
354159
+ const full = join4(PENDING_CLEANUP_DIR, file);
353975
354160
  try {
353976
354161
  const raw = await readFile(full, "utf-8");
353977
354162
  const marker = JSON.parse(raw);
@@ -354024,7 +354209,7 @@ async function startMobileNetworkCapture(options) {
354024
354209
  let caKeyPem = null;
354025
354210
  let caCertPem = null;
354026
354211
  try {
354027
- tmpDir = await mkdtemp(join3(tmpdir2(), "validateqa-mobile-ca-"));
354212
+ tmpDir = await mkdtemp(join4(tmpdir2(), "validateqa-mobile-ca-"));
354028
354213
  const ca = await generateCaCert(tmpDir, onLog);
354029
354214
  if (ca) {
354030
354215
  caCertPath = ca.caCertPath;
@@ -354032,7 +354217,7 @@ async function startMobileNetworkCapture(options) {
354032
354217
  caCertPem = ca.caCertPem;
354033
354218
  }
354034
354219
  } catch (err) {
354035
- onLog?.(`[mobile-net] CA setup degraded: ${errMsg(err)}`);
354220
+ onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
354036
354221
  }
354037
354222
  if (!caKeyPem || !caCertPem) {
354038
354223
  try {
@@ -354040,9 +354225,9 @@ async function startMobileNetworkCapture(options) {
354040
354225
  caKeyPem = fallback.key;
354041
354226
  caCertPem = fallback.cert;
354042
354227
  } catch (err) {
354043
- onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg(err)}`);
354228
+ onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
354044
354229
  if (tmpDir) {
354045
- await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
354230
+ await rm2(tmpDir, { recursive: true, force: true }).catch(() => void 0);
354046
354231
  }
354047
354232
  throw err instanceof Error ? err : new Error(String(err));
354048
354233
  }
@@ -354097,7 +354282,7 @@ async function startMobileNetworkCapture(options) {
354097
354282
  }
354098
354283
  }
354099
354284
  } catch (err) {
354100
- wiringReason = `device wiring degraded: ${errMsg(err)}`;
354285
+ wiringReason = `device wiring degraded: ${errMsg2(err)}`;
354101
354286
  onLog?.(`[mobile-net] ${wiringReason}`);
354102
354287
  }
354103
354288
  if (proxySet || caInstalledOnDevice || hostProxySet) {
@@ -354186,9 +354371,9 @@ async function startMobileNetworkCapture(options) {
354186
354371
  await proxy.close();
354187
354372
  if (capturedTmpDir) {
354188
354373
  try {
354189
- await rm(capturedTmpDir, { recursive: true, force: true });
354374
+ await rm2(capturedTmpDir, { recursive: true, force: true });
354190
354375
  } catch (err) {
354191
- onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg(err)}`);
354376
+ onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
354192
354377
  }
354193
354378
  }
354194
354379
  const apiCount = finalCalls.filter((c) => c.isApi).length;
@@ -354225,7 +354410,7 @@ var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
354225
354410
  var WD_ACTION_TIMEOUT_MS = 3e4;
354226
354411
  var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
354227
354412
  var MAX_SESSION_CREATE_ATTEMPTS = 3;
354228
- var execFileAsync2 = promisify2(execFile2);
354413
+ var execFileAsync3 = promisify3(execFile3);
354229
354414
  var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
354230
354415
  var WebDriverTransportError = class extends Error {
354231
354416
  isTransport = true;
@@ -354354,11 +354539,11 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
354354
354539
  return { "appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200) };
354355
354540
  }
354356
354541
  const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
354357
- const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join4(tmpdir3(), "validateqa-wda");
354542
+ const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
354358
354543
  return {
354359
354544
  "appium:wdaLocalPort": readPort("APPIUM_WDA_LOCAL_PORT", 8100),
354360
354545
  "appium:mjpegServerPort": readPort("APPIUM_MJPEG_SERVER_PORT", 9100),
354361
- "appium:derivedDataPath": join4(derivedDataRoot, safeDevice)
354546
+ "appium:derivedDataPath": join5(derivedDataRoot, safeDevice)
354362
354547
  };
354363
354548
  }
354364
354549
  function isAndroidSystemPortBusyError(error2) {
@@ -354442,7 +354627,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
354442
354627
  const args = androidSystemPortForwardRemovalArgs(target, error2);
354443
354628
  if (!args) return false;
354444
354629
  try {
354445
- await execFileAsync2("adb", args, { timeout: 1e4, windowsHide: true });
354630
+ await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
354446
354631
  return true;
354447
354632
  } catch {
354448
354633
  return false;
@@ -354700,7 +354885,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
354700
354885
  if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
354701
354886
  return boundsFromStep(step);
354702
354887
  }
354703
- function locatorStrategyForValue(value) {
354888
+ function locatorStrategyForValue(value, platform3) {
354704
354889
  const trimmed = value?.trim();
354705
354890
  if (!trimmed) {
354706
354891
  return [];
@@ -354716,7 +354901,8 @@ function locatorStrategyForValue(value) {
354716
354901
  }
354717
354902
  return [
354718
354903
  { using: "accessibility id", value: trimmed },
354719
- { using: "id", value: trimmed }
354904
+ { using: "id", value: trimmed },
354905
+ ...textIdentityStrategies(trimmed, platform3)
354720
354906
  ];
354721
354907
  }
354722
354908
  function buildLocatorStrategies(target, platform3) {
@@ -354726,19 +354912,19 @@ function buildLocatorStrategies(target, platform3) {
354726
354912
  if (bundle) {
354727
354913
  if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
354728
354914
  if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
354915
+ if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354729
354916
  for (const value of [
354730
- isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
354917
+ isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
354731
354918
  bundle.contentDesc
354732
354919
  ]) {
354733
354920
  const trimmed = value?.trim();
354734
- if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
354921
+ if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
354735
354922
  }
354736
- if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
354737
354923
  }
354738
354924
  if (!(platform3 && hasPlatformLocator(target, platform3))) {
354739
- strategies.push(...locatorStrategyForValue(target.primary));
354925
+ strategies.push(...locatorStrategyForValue(target.primary, platform3));
354740
354926
  for (const fallback of target.fallbacks ?? []) {
354741
- strategies.push(...locatorStrategyForValue(fallback));
354927
+ strategies.push(...locatorStrategyForValue(fallback, platform3));
354742
354928
  }
354743
354929
  }
354744
354930
  return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
@@ -354760,12 +354946,31 @@ function identityTextXpath2(value) {
354760
354946
  const lit = xpathLiteral3(value);
354761
354947
  return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
354762
354948
  }
354949
+ function iosPredicateLiteral(value) {
354950
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
354951
+ }
354952
+ function iosTextIdentityPredicate(value) {
354953
+ const lit = iosPredicateLiteral(value);
354954
+ return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
354955
+ }
354956
+ function textIdentityStrategies(value, platform3) {
354957
+ const strategies = [];
354958
+ if (platform3 === "IOS") {
354959
+ strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
354960
+ }
354961
+ strategies.push({ using: "xpath", value: identityTextXpath2(value) });
354962
+ return strategies;
354963
+ }
354964
+ function isIdentityIosPredicate(value) {
354965
+ return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
354966
+ }
354763
354967
  function isIdentityXpath(value) {
354764
354968
  return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
354765
354969
  }
354766
354970
  function isIdentityStrategy(strategy) {
354767
354971
  if (strategy.using === "accessibility id" || strategy.using === "id") return true;
354768
354972
  if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
354973
+ if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
354769
354974
  return false;
354770
354975
  }
354771
354976
  function buildIdentityLocatorStrategies(target, platform3) {
@@ -354775,15 +354980,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
354775
354980
  if (bundle) {
354776
354981
  if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
354777
354982
  if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
354778
- for (const value of [bundle.text, bundle.contentDesc]) {
354983
+ if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354984
+ for (const value of [
354985
+ isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
354986
+ bundle.contentDesc
354987
+ ]) {
354779
354988
  const trimmed = value?.trim();
354780
- if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
354989
+ if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
354781
354990
  }
354782
- if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354783
354991
  }
354784
354992
  if (!(platform3 && hasPlatformLocator(target, platform3))) {
354785
354993
  for (const value of [target.primary, ...target.fallbacks ?? []]) {
354786
- for (const strategy of locatorStrategyForValue(value)) {
354994
+ for (const strategy of locatorStrategyForValue(value, platform3)) {
354787
354995
  if (isIdentityStrategy(strategy)) strategies.push(strategy);
354788
354996
  }
354789
354997
  }
@@ -354893,7 +355101,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
354893
355101
  ];
354894
355102
  for (const [namespace, key, value] of settingsToDisable) {
354895
355103
  try {
354896
- await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
355104
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
354897
355105
  timeout: 8e3,
354898
355106
  windowsHide: true
354899
355107
  });
@@ -354905,14 +355113,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
354905
355113
  if (!deviceId) {
354906
355114
  throw new Error("Android keyboard fallback requires a device id");
354907
355115
  }
354908
- await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
355116
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
354909
355117
  timeout: 1e4,
354910
355118
  windowsHide: true
354911
355119
  });
354912
355120
  }
354913
355121
  async function pressAndroidKeyboardEnter(deviceId) {
354914
355122
  if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
354915
- await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
355123
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
354916
355124
  timeout: 1e4,
354917
355125
  windowsHide: true
354918
355126
  });
@@ -355433,7 +355641,14 @@ async function executeMobileTest(options) {
355433
355641
  const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
355434
355642
  if (resetAllowed) {
355435
355643
  try {
355436
- await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
355644
+ if (options.target.platform === "IOS") {
355645
+ await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
355646
+ });
355647
+ const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
355648
+ if (!wiped.ok) throw new Error(wiped.detail);
355649
+ } else {
355650
+ await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
355651
+ }
355437
355652
  clearedAppDataBeforeLaunch = true;
355438
355653
  log2(resetPolicy === "CLEAR_APP_DATA" ? "App data cleared before launch (target resetPolicy=CLEAR_APP_DATA)." : "App data cleared before launch (test was recorded from a cold logged-out state).");
355439
355654
  } catch (err) {
@@ -355778,8 +355993,7 @@ async function readElementAttribute(sessionId, elementId, name) {
355778
355993
  function harvestFallbackXpath(platform3, attrs) {
355779
355994
  if (platform3 === "IOS") {
355780
355995
  if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
355781
- const text = attrs.label ?? attrs.value;
355782
- if (text) return identityTextXpath2(text);
355996
+ if (attrs.text) return identityTextXpath2(attrs.text);
355783
355997
  return "//*";
355784
355998
  }
355785
355999
  if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
@@ -355789,14 +356003,15 @@ function harvestFallbackXpath(platform3, attrs) {
355789
356003
  }
355790
356004
  async function readNativeElementBundle(sessionId, elementId, platform3) {
355791
356005
  const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
356006
+ const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
355792
356007
  if (platform3 === "IOS") {
355793
356008
  const name = await readElementAttribute(sessionId, elementId, "name");
355794
356009
  const label = await readElementAttribute(sessionId, elementId, "label");
355795
356010
  const value = await readElementAttribute(sessionId, elementId, "value");
355796
- const text2 = label ?? value;
356011
+ const text2 = label ?? (volatileText ? void 0 : value);
355797
356012
  if (!name && !text2) return null;
355798
356013
  const bundle2 = {
355799
- xpath: harvestFallbackXpath("IOS", { name, label, value }),
356014
+ xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
355800
356015
  className
355801
356016
  };
355802
356017
  if (name) bundle2.accessibilityId = name;
@@ -355805,7 +356020,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
355805
356020
  }
355806
356021
  const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
355807
356022
  const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
355808
- const text = await readElementAttribute(sessionId, elementId, "text");
356023
+ const rawText = await readElementAttribute(sessionId, elementId, "text");
356024
+ const text = volatileText ? void 0 : rawText;
355809
356025
  if (!resourceId && !contentDesc && !text) return null;
355810
356026
  const bundle = {
355811
356027
  xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
@@ -356093,7 +356309,7 @@ import { createServer } from "http";
356093
356309
  import { execFileSync as execFileSync4 } from "child_process";
356094
356310
  import { randomBytes, timingSafeEqual } from "crypto";
356095
356311
  import { homedir as homedir2 } from "os";
356096
- import { join as join5, dirname } from "path";
356312
+ import { join as join6, dirname } from "path";
356097
356313
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
356098
356314
  init_dist();
356099
356315
  var MOBILE_BRIDGE_PORT2 = 9515;
@@ -356200,7 +356416,7 @@ async function getWindowSize3() {
356200
356416
  }
356201
356417
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
356202
356418
  var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
356203
- var BRIDGE_PID_FILE = join5(homedir2(), ".validate.qa", "mobile-bridge.pid");
356419
+ var BRIDGE_PID_FILE = join6(homedir2(), ".validate.qa", "mobile-bridge.pid");
356204
356420
  function readBridgePidFile() {
356205
356421
  try {
356206
356422
  const raw = readFileSync2(BRIDGE_PID_FILE, "utf-8").trim();
@@ -357338,6 +357554,7 @@ var MobileBridgeDriver = class {
357338
357554
  sessionId;
357339
357555
  platform;
357340
357556
  appId;
357557
+ deviceId;
357341
357558
  allowAppReset;
357342
357559
  recoverSession;
357343
357560
  maxSessionRecoveries;
@@ -357358,6 +357575,7 @@ var MobileBridgeDriver = class {
357358
357575
  this.sessionId = config.sessionId;
357359
357576
  this.platform = config.platform;
357360
357577
  this.appId = config.appId;
357578
+ this.deviceId = config.deviceId;
357361
357579
  this.allowAppReset = config.allowAppReset === true;
357362
357580
  this.recoverSession = config.recoverSession;
357363
357581
  this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
@@ -357655,17 +357873,34 @@ var MobileBridgeDriver = class {
357655
357873
  });
357656
357874
  }
357657
357875
  /**
357658
- * Deterministic state reset: clear app data + relaunch (Android emulators
357659
- * only, per the runner-computed allowAppReset policy). Returns false when
357660
- * unsupported/disallowed or when clearApp throws — callers fall back to
357661
- * relaunch(), preserving today's behavior. On success, waits for cold-boot
357662
- * content so recording and replay agree on the true entry screen.
357876
+ * Deterministic state reset: clear app data + relaunch, per the
357877
+ * runner-computed allowAppReset policy (Android emulators / iOS simulators
357878
+ * only — a personal physical device is never wiped). Android clears via
357879
+ * `mobile: clearApp`; iOS terminates the app and wipes its simulator data
357880
+ * container + keychain through `simctl` (see ios-simulator-reset.ts).
357881
+ * Returns false when unsupported/disallowed or when the clear fails —
357882
+ * callers fall back to relaunch(), preserving today's behavior. On success,
357883
+ * waits for cold-boot content so recording and replay agree on the true
357884
+ * entry screen.
357663
357885
  */
357664
357886
  async resetAppState() {
357665
- if (!this.allowAppReset || this.platform !== "ANDROID") return false;
357887
+ if (!this.allowAppReset) return false;
357888
+ if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
357666
357889
  return this.withSessionRecovery("app data reset", async () => {
357667
357890
  try {
357668
- await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
357891
+ if (this.platform === "IOS") {
357892
+ try {
357893
+ await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
357894
+ } catch (err) {
357895
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
357896
+ }
357897
+ const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
357898
+ if (!wiped.ok) {
357899
+ throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
357900
+ }
357901
+ } else {
357902
+ await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
357903
+ }
357669
357904
  await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
357670
357905
  try {
357671
357906
  await waitForColdBootSettle(this.sessionPath);
@@ -358331,7 +358566,7 @@ var log = {
358331
358566
  dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
358332
358567
  };
358333
358568
  function getRunsDir() {
358334
- const dir = process.env.RUNS_DIR || join6(homedir3(), ".validate.qa", "runs");
358569
+ const dir = process.env.RUNS_DIR || join7(homedir3(), ".validate.qa", "runs");
358335
358570
  (0, import_runner_core12.ensurePrivateDirectory)(dir);
358336
358571
  return dir;
358337
358572
  }
@@ -360263,9 +360498,11 @@ ${finalVerifyResult.logs ?? ""}`;
360263
360498
  sessionId: session.sessionId,
360264
360499
  platform: platform3,
360265
360500
  appId: session.appId,
360266
- // Deterministic-state authorization: Android emulators only by
360267
- // default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
360268
- // physical device must never have its app data wiped.
360501
+ deviceId: session.deviceId,
360502
+ // Deterministic-state authorization: Android emulators / iOS
360503
+ // simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
360504
+ // Android default) — a personal physical device must never have
360505
+ // its app data wiped.
360269
360506
  allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
360270
360507
  maxSessionRecoveries: 1,
360271
360508
  recoverSession: async (cause) => {
@@ -360781,6 +361018,7 @@ ${finalVerifyResult.logs ?? ""}`;
360781
361018
  sessionId: opened.sessionId,
360782
361019
  platform: target.platform,
360783
361020
  appId: opened.appId,
361021
+ deviceId: opened.deviceId,
360784
361022
  allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
360785
361023
  });
360786
361024
  };