@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.js CHANGED
@@ -978,7 +978,7 @@ var require_mobile_source_parser = __commonJS({
978
978
  const value = nonEmpty3(attrs.value);
979
979
  const label = name ?? labelAttr ?? value;
980
980
  const accessibilityId = name;
981
- const text = value && value !== label ? value : labelAttr && labelAttr !== label ? labelAttr : void 0;
981
+ const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
982
982
  const bounds = parseIOSBounds(attrs);
983
983
  const enabled = attrBoolDefaultTrue(attrs.enabled);
984
984
  const visible = attrBoolDefaultTrue(attrs.visible);
@@ -1125,6 +1125,42 @@ var require_mobile_source_parser = __commonJS({
1125
1125
  }
1126
1126
  }
1127
1127
  }
1128
+ function annotateIdentityUniqueness(elements) {
1129
+ const a11yCounts = /* @__PURE__ */ new Map();
1130
+ const ridCounts = /* @__PURE__ */ new Map();
1131
+ const textCounts = /* @__PURE__ */ new Map();
1132
+ const bump = (map2, value) => {
1133
+ const key = value?.trim();
1134
+ if (!key)
1135
+ return;
1136
+ map2.set(key, (map2.get(key) ?? 0) + 1);
1137
+ };
1138
+ for (const element of elements) {
1139
+ bump(a11yCounts, element.accessibilityId);
1140
+ bump(ridCounts, element.resourceId);
1141
+ const identityValues = new Set([element.text, element.accessibilityId].map((value) => value?.trim()).filter((value) => Boolean(value)));
1142
+ for (const value of identityValues)
1143
+ bump(textCounts, value);
1144
+ }
1145
+ const isUnique = (map2, value) => {
1146
+ const key = value?.trim();
1147
+ if (!key)
1148
+ return void 0;
1149
+ return (map2.get(key) ?? 0) <= 1;
1150
+ };
1151
+ for (const element of elements) {
1152
+ const accessibilityId = isUnique(a11yCounts, element.accessibilityId);
1153
+ const resourceId = isUnique(ridCounts, element.resourceId);
1154
+ const text = isUnique(textCounts, element.text);
1155
+ if (accessibilityId === void 0 && resourceId === void 0 && text === void 0)
1156
+ continue;
1157
+ element.identityUnique = {
1158
+ ...accessibilityId !== void 0 ? { accessibilityId } : {},
1159
+ ...resourceId !== void 0 ? { resourceId } : {},
1160
+ ...text !== void 0 ? { text } : {}
1161
+ };
1162
+ }
1163
+ }
1128
1164
  function flatten(root, platform3) {
1129
1165
  const out = [];
1130
1166
  const stack = [];
@@ -1170,6 +1206,7 @@ var require_mobile_source_parser = __commonJS({
1170
1206
  }
1171
1207
  }
1172
1208
  markRepeatedBottomBarNavigation(out);
1209
+ annotateIdentityUniqueness(out);
1173
1210
  return out;
1174
1211
  }
1175
1212
  function fnv1a(input) {
@@ -1251,6 +1288,13 @@ var require_mobile_source_parser = __commonJS({
1251
1288
  function isEditableElement(el) {
1252
1289
  return EDITABLE_CLASS_RE.test(el.role.trim());
1253
1290
  }
1291
+ var STATE_VALUE_CLASS_RE = /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/;
1292
+ function isStateValueRole(role) {
1293
+ return STATE_VALUE_CLASS_RE.test(role.trim());
1294
+ }
1295
+ function isStateValueElement(el) {
1296
+ return isStateValueRole(el.role);
1297
+ }
1254
1298
  function xpathAttrLiteral(value) {
1255
1299
  if (!value.includes("'"))
1256
1300
  return `'${value}'`;
@@ -1261,17 +1305,18 @@ var require_mobile_source_parser = __commonJS({
1261
1305
  function semanticXPath(el, platform3) {
1262
1306
  const cls = nonBlank(el.role) ?? "*";
1263
1307
  const a11y = nonBlank(el.accessibilityId);
1264
- if (a11y) {
1265
- return platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]`;
1266
- }
1267
- const rid = nonBlank(el.resourceId);
1268
- if (platform3 === "ANDROID" && rid)
1269
- return `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]`;
1270
- const text = nonBlank(el.text);
1271
- if (text && !isEditableElement(el)) {
1272
- return platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]`;
1273
- }
1274
- return void 0;
1308
+ const rid = platform3 === "ANDROID" ? nonBlank(el.resourceId) : void 0;
1309
+ const text = isEditableElement(el) || isStateValueElement(el) ? void 0 : nonBlank(el.text);
1310
+ const a11yXpath = a11y ? platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]` : void 0;
1311
+ const ridXpath = rid ? `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]` : void 0;
1312
+ const textXpath = text ? platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]` : void 0;
1313
+ const candidates = [
1314
+ { xpath: a11yXpath, unique: el.identityUnique?.accessibilityId },
1315
+ { xpath: ridXpath, unique: el.identityUnique?.resourceId },
1316
+ { xpath: textXpath, unique: el.identityUnique?.text }
1317
+ ];
1318
+ const uniqueFirst = candidates.find((c) => c.xpath && c.unique !== false);
1319
+ return (uniqueFirst ?? candidates.find((c) => c.xpath))?.xpath;
1275
1320
  }
1276
1321
  function nonBlank(value) {
1277
1322
  if (typeof value !== "string")
@@ -1280,30 +1325,40 @@ var require_mobile_source_parser = __commonJS({
1280
1325
  return trimmed.length > 0 ? trimmed : void 0;
1281
1326
  }
1282
1327
  function buildLocatorBundle(el, platform3) {
1328
+ const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
1283
1329
  const bundle = {
1284
1330
  // Attribute-anchored xpath when the element has identity; the absolute
1285
1331
  // indexed path only for identity-less elements (nothing else can find them).
1286
1332
  xpath: semanticXPath(el, platform3) ?? el.xpath,
1287
1333
  className: el.role
1288
1334
  };
1289
- if (el.accessibilityId)
1335
+ const a11yUnique = el.identityUnique?.accessibilityId !== false;
1336
+ const ridUnique = el.identityUnique?.resourceId !== false;
1337
+ const textUnique = el.identityUnique?.text !== false;
1338
+ const hasUniqueAlternativeToA11y = Boolean(el.resourceId && platform3 === "ANDROID" && ridUnique || durableText && textUnique);
1339
+ const hasUniqueAlternativeToRid = Boolean(el.accessibilityId && a11yUnique || durableText && textUnique);
1340
+ const includeA11y = Boolean(el.accessibilityId && (a11yUnique || !hasUniqueAlternativeToA11y));
1341
+ const includeRid = Boolean(platform3 === "ANDROID" && el.resourceId && (ridUnique || !hasUniqueAlternativeToRid));
1342
+ if (includeA11y && el.accessibilityId)
1290
1343
  bundle.accessibilityId = el.accessibilityId;
1291
- if (platform3 === "ANDROID" && el.resourceId)
1344
+ if (includeRid && el.resourceId)
1292
1345
  bundle.resourceId = el.resourceId;
1293
- if (el.text && !isEditableElement(el))
1294
- bundle.text = el.text;
1295
- if (platform3 === "ANDROID" && el.accessibilityId)
1346
+ if (durableText)
1347
+ bundle.text = durableText;
1348
+ if (includeA11y && platform3 === "ANDROID" && el.accessibilityId)
1296
1349
  bundle.contentDesc = el.accessibilityId;
1297
1350
  return bundle;
1298
1351
  }
1299
1352
  function preferredLocator(el) {
1300
- if (el.accessibilityId)
1301
- return el.accessibilityId;
1302
- if (el.resourceId)
1303
- return el.resourceId;
1304
- if (el.text && !isEditableElement(el))
1305
- return el.text;
1306
- return el.xpath;
1353
+ const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
1354
+ const candidates = [
1355
+ { value: el.accessibilityId, unique: el.identityUnique?.accessibilityId },
1356
+ { value: el.resourceId, unique: el.identityUnique?.resourceId },
1357
+ { value: durableText, unique: el.identityUnique?.text }
1358
+ ];
1359
+ const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
1360
+ const chosen = (uniqueFirst ?? candidates.find((c) => c.value))?.value;
1361
+ return chosen ?? el.xpath;
1307
1362
  }
1308
1363
  function buildScreenSignal(elements, root, platform3) {
1309
1364
  const signal = {
@@ -1966,6 +2021,11 @@ function isEditableMobileClassName(className) {
1966
2021
  return false;
1967
2022
  return /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/.test(className.trim());
1968
2023
  }
2024
+ function isStateValueMobileClassName(className) {
2025
+ if (!className)
2026
+ return false;
2027
+ return /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/.test(className.trim());
2028
+ }
1969
2029
  function mobileCredentialPlaceholder(key) {
1970
2030
  return `{{${key}}}`;
1971
2031
  }
@@ -2095,9 +2155,10 @@ function buildStepTarget(metadata, selector) {
2095
2155
  locatorBundle.resourceId,
2096
2156
  // For editable fields the text attribute IS the currently-typed value — a
2097
2157
  // self-invalidating locator that only matches while the recorded text is
2098
- // still in the field (and it can leak typed input into locators). Static
2099
- // elements keep their text fallback.
2100
- isEditableMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
2158
+ // still in the field (and it can leak typed input into locators). State
2159
+ // controls' text is their current state ("1"/"ON") and collides with every
2160
+ // same-state sibling. Static elements keep their text fallback.
2161
+ isEditableMobileClassName(locatorBundle.className) || isStateValueMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
2101
2162
  locatorBundle.contentDesc,
2102
2163
  locatorBundle.xpath,
2103
2164
  selector
@@ -2253,7 +2314,9 @@ function mobileLocatorBundleHasIdentity(bundle) {
2253
2314
  return false;
2254
2315
  if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
2255
2316
  return true;
2256
- if (nonEmpty(bundle.text) || nonEmpty(bundle.contentDesc))
2317
+ if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
2318
+ return true;
2319
+ if (nonEmpty(bundle.contentDesc))
2257
2320
  return true;
2258
2321
  const xpath = nonEmpty(bundle.xpath);
2259
2322
  return !!xpath && isIdentityMobileXpath(xpath);
@@ -2274,23 +2337,27 @@ function selectorCandidatesForStep(step) {
2274
2337
  candidates.push(`~${bundle.accessibilityId}`);
2275
2338
  if (nonEmpty(bundle.resourceId))
2276
2339
  candidates.push(`id=${bundle.resourceId}`);
2340
+ if (nonEmpty(bundle.xpath))
2341
+ candidates.push(bundle.xpath);
2277
2342
  const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
2278
- const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
2343
+ const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
2279
2344
  for (const value of [textIdentity, bundle.contentDesc]) {
2280
2345
  const trimmed = nonEmpty(value);
2281
2346
  if (trimmed)
2282
2347
  candidates.push(identityTextXpath(trimmed));
2283
2348
  }
2284
- if (nonEmpty(bundle.xpath))
2285
- candidates.push(bundle.xpath);
2286
2349
  }
2287
2350
  const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
2351
+ const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
2288
2352
  const pushValueCandidates = (value) => {
2289
2353
  candidates.push(selectorForValue(value));
2290
2354
  const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
2291
2355
  if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
2292
2356
  candidates.push(`id=${value}`);
2293
2357
  }
2358
+ if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
2359
+ candidates.push(identityTextXpath(value));
2360
+ }
2294
2361
  };
2295
2362
  if (nonEmpty(target.primary))
2296
2363
  pushValueCandidates(target.primary);
@@ -2985,7 +3052,8 @@ function isWebViewStep(step) {
2985
3052
  function hasCrossPlatformIdentity(bundle) {
2986
3053
  if (!bundle)
2987
3054
  return false;
2988
- return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
3055
+ const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
3056
+ return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
2989
3057
  }
2990
3058
  function stepIsDivergentForPlatform(step, platform3) {
2991
3059
  return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
@@ -3067,7 +3135,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
3067
3135
  function hasStableNativeIdentity(bundle) {
3068
3136
  if (!bundle)
3069
3137
  return false;
3070
- 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));
3138
+ const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
3139
+ 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));
3071
3140
  }
3072
3141
  function isPositionalXpath(xpath) {
3073
3142
  const value = xpath?.trim();
@@ -3147,6 +3216,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
3147
3216
  var init_mobile_cross_platform = __esm({
3148
3217
  "../shared/dist/mobile-cross-platform.js"() {
3149
3218
  "use strict";
3219
+ init_mobile_test_generation();
3150
3220
  MOBILE_PLATFORMS = ["IOS", "ANDROID"];
3151
3221
  LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
3152
3222
  "tap",
@@ -6149,6 +6219,7 @@ __export(dist_exports, {
6149
6219
  isEditableMobileClassName: () => isEditableMobileClassName,
6150
6220
  isEmailFromIdentity: () => isEmailFromIdentity,
6151
6221
  isEnvironmentalError: () => isEnvironmentalError,
6222
+ isIdentityMobileXpath: () => isIdentityMobileXpath,
6152
6223
  isMobilePlatform: () => isMobilePlatform,
6153
6224
  isPricingPlan: () => isPricingPlan,
6154
6225
  isRunnableStep: () => isRunnableStep,
@@ -6156,6 +6227,7 @@ __export(dist_exports, {
6156
6227
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
6157
6228
  isSensitiveQueryKey: () => isSensitiveQueryKey,
6158
6229
  isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
6230
+ isStateValueMobileClassName: () => isStateValueMobileClassName,
6159
6231
  isSttModelForProvider: () => isSttModelForProvider,
6160
6232
  isTestReadyForPlatform: () => isTestReadyForPlatform,
6161
6233
  isTrialExpired: () => isTrialExpired,
@@ -7063,6 +7135,7 @@ var require_mobile_observation = __commonJS({
7063
7135
  exports2.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
7064
7136
  exports2.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
7065
7137
  exports2.safeMobileElementLabel = safeMobileElementLabel;
7138
+ var mobile_source_parser_js_1 = require_mobile_source_parser();
7066
7139
  var DEFAULT_MAX_ELEMENTS = 60;
7067
7140
  var MAX_LABEL_LENGTH = 80;
7068
7141
  var EVIDENCE_LABEL_LIMIT = 40;
@@ -7153,7 +7226,8 @@ var require_mobile_observation = __commonJS({
7153
7226
  const locators = [];
7154
7227
  if (stableId)
7155
7228
  locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
7156
- const xpath = element.xpath?.trim();
7229
+ const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
7230
+ const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
7157
7231
  if (xpath)
7158
7232
  locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
7159
7233
  const collected = {
@@ -11219,7 +11293,7 @@ var require_thread_stream = __commonJS({
11219
11293
  var { version } = require_package();
11220
11294
  var { EventEmitter: EventEmitter2 } = require("events");
11221
11295
  var { Worker } = require("worker_threads");
11222
- var { join: join7 } = require("path");
11296
+ var { join: join8 } = require("path");
11223
11297
  var { pathToFileURL } = require("url");
11224
11298
  var { wait } = require_wait();
11225
11299
  var {
@@ -11270,7 +11344,7 @@ var require_thread_stream = __commonJS({
11270
11344
  function createWorker(stream, opts) {
11271
11345
  const { filename, workerData } = opts;
11272
11346
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
11273
- const toExecute = bundlerOverrides["thread-stream-worker"] || join7(__dirname, "lib", "worker.js");
11347
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
11274
11348
  const worker = new Worker(toExecute, {
11275
11349
  ...opts.workerOpts,
11276
11350
  name: opts.workerOpts?.name || "thread-stream",
@@ -11738,7 +11812,7 @@ var require_transport = __commonJS({
11738
11812
  var { createRequire } = require("module");
11739
11813
  var { existsSync: existsSync3 } = require("fs");
11740
11814
  var getCallers = require_caller();
11741
- var { join: join7, isAbsolute, sep } = require("path");
11815
+ var { join: join8, isAbsolute: isAbsolute2, sep } = require("path");
11742
11816
  var { fileURLToPath } = require("url");
11743
11817
  var sleep = require_atomic_sleep();
11744
11818
  var onExit = require_on_exit_leak_free();
@@ -11810,7 +11884,7 @@ var require_transport = __commonJS({
11810
11884
  return false;
11811
11885
  }
11812
11886
  }
11813
- return isAbsolute(path) && !existsSync3(path);
11887
+ return isAbsolute2(path) && !existsSync3(path);
11814
11888
  }
11815
11889
  function stripQuotes(value) {
11816
11890
  const first = value[0];
@@ -11891,7 +11965,7 @@ var require_transport = __commonJS({
11891
11965
  throw new Error("only one of target or targets can be specified");
11892
11966
  }
11893
11967
  if (targets) {
11894
- target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
11968
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
11895
11969
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
11896
11970
  return {
11897
11971
  ...dest,
@@ -11909,7 +11983,7 @@ var require_transport = __commonJS({
11909
11983
  });
11910
11984
  });
11911
11985
  } else if (pipeline) {
11912
- target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
11986
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
11913
11987
  options.pipelines = [pipeline.map((dest) => {
11914
11988
  return {
11915
11989
  ...dest,
@@ -11928,11 +12002,11 @@ var require_transport = __commonJS({
11928
12002
  return buildStream(fixTarget(target), options, worker, sync, name);
11929
12003
  function fixTarget(origin) {
11930
12004
  origin = bundlerOverrides[origin] || origin;
11931
- if (isAbsolute(origin) || origin.indexOf("file://") === 0) {
12005
+ if (isAbsolute2(origin) || origin.indexOf("file://") === 0) {
11932
12006
  return origin;
11933
12007
  }
11934
12008
  if (origin === "pino/file") {
11935
- return join7(__dirname, "..", "file.js");
12009
+ return join8(__dirname, "..", "file.js");
11936
12010
  }
11937
12011
  let fixTarget2;
11938
12012
  for (const filePath of callers) {
@@ -12913,7 +12987,7 @@ var require_safe_stable_stringify = __commonJS({
12913
12987
  return circularValue;
12914
12988
  }
12915
12989
  let res = "";
12916
- let join7 = ",";
12990
+ let join8 = ",";
12917
12991
  const originalIndentation = indentation;
12918
12992
  if (Array.isArray(value)) {
12919
12993
  if (value.length === 0) {
@@ -12927,7 +13001,7 @@ var require_safe_stable_stringify = __commonJS({
12927
13001
  indentation += spacer;
12928
13002
  res += `
12929
13003
  ${indentation}`;
12930
- join7 = `,
13004
+ join8 = `,
12931
13005
  ${indentation}`;
12932
13006
  }
12933
13007
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -12935,13 +13009,13 @@ ${indentation}`;
12935
13009
  for (; i < maximumValuesToStringify - 1; i++) {
12936
13010
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
12937
13011
  res += tmp2 !== void 0 ? tmp2 : "null";
12938
- res += join7;
13012
+ res += join8;
12939
13013
  }
12940
13014
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
12941
13015
  res += tmp !== void 0 ? tmp : "null";
12942
13016
  if (value.length - 1 > maximumBreadth) {
12943
13017
  const removedKeys = value.length - maximumBreadth - 1;
12944
- res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
13018
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
12945
13019
  }
12946
13020
  if (spacer !== "") {
12947
13021
  res += `
@@ -12962,7 +13036,7 @@ ${originalIndentation}`;
12962
13036
  let separator = "";
12963
13037
  if (spacer !== "") {
12964
13038
  indentation += spacer;
12965
- join7 = `,
13039
+ join8 = `,
12966
13040
  ${indentation}`;
12967
13041
  whitespace = " ";
12968
13042
  }
@@ -12976,13 +13050,13 @@ ${indentation}`;
12976
13050
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
12977
13051
  if (tmp !== void 0) {
12978
13052
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
12979
- separator = join7;
13053
+ separator = join8;
12980
13054
  }
12981
13055
  }
12982
13056
  if (keyLength > maximumBreadth) {
12983
13057
  const removedKeys = keyLength - maximumBreadth;
12984
13058
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
12985
- separator = join7;
13059
+ separator = join8;
12986
13060
  }
12987
13061
  if (spacer !== "" && separator.length > 1) {
12988
13062
  res = `
@@ -13023,7 +13097,7 @@ ${originalIndentation}`;
13023
13097
  }
13024
13098
  const originalIndentation = indentation;
13025
13099
  let res = "";
13026
- let join7 = ",";
13100
+ let join8 = ",";
13027
13101
  if (Array.isArray(value)) {
13028
13102
  if (value.length === 0) {
13029
13103
  return "[]";
@@ -13036,7 +13110,7 @@ ${originalIndentation}`;
13036
13110
  indentation += spacer;
13037
13111
  res += `
13038
13112
  ${indentation}`;
13039
- join7 = `,
13113
+ join8 = `,
13040
13114
  ${indentation}`;
13041
13115
  }
13042
13116
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -13044,13 +13118,13 @@ ${indentation}`;
13044
13118
  for (; i < maximumValuesToStringify - 1; i++) {
13045
13119
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
13046
13120
  res += tmp2 !== void 0 ? tmp2 : "null";
13047
- res += join7;
13121
+ res += join8;
13048
13122
  }
13049
13123
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
13050
13124
  res += tmp !== void 0 ? tmp : "null";
13051
13125
  if (value.length - 1 > maximumBreadth) {
13052
13126
  const removedKeys = value.length - maximumBreadth - 1;
13053
- res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
13127
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
13054
13128
  }
13055
13129
  if (spacer !== "") {
13056
13130
  res += `
@@ -13063,7 +13137,7 @@ ${originalIndentation}`;
13063
13137
  let whitespace = "";
13064
13138
  if (spacer !== "") {
13065
13139
  indentation += spacer;
13066
- join7 = `,
13140
+ join8 = `,
13067
13141
  ${indentation}`;
13068
13142
  whitespace = " ";
13069
13143
  }
@@ -13072,7 +13146,7 @@ ${indentation}`;
13072
13146
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
13073
13147
  if (tmp !== void 0) {
13074
13148
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
13075
- separator = join7;
13149
+ separator = join8;
13076
13150
  }
13077
13151
  }
13078
13152
  if (spacer !== "" && separator.length > 1) {
@@ -13130,20 +13204,20 @@ ${originalIndentation}`;
13130
13204
  indentation += spacer;
13131
13205
  let res2 = `
13132
13206
  ${indentation}`;
13133
- const join8 = `,
13207
+ const join9 = `,
13134
13208
  ${indentation}`;
13135
13209
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
13136
13210
  let i = 0;
13137
13211
  for (; i < maximumValuesToStringify - 1; i++) {
13138
13212
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
13139
13213
  res2 += tmp2 !== void 0 ? tmp2 : "null";
13140
- res2 += join8;
13214
+ res2 += join9;
13141
13215
  }
13142
13216
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
13143
13217
  res2 += tmp !== void 0 ? tmp : "null";
13144
13218
  if (value.length - 1 > maximumBreadth) {
13145
13219
  const removedKeys = value.length - maximumBreadth - 1;
13146
- res2 += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
13220
+ res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
13147
13221
  }
13148
13222
  res2 += `
13149
13223
  ${originalIndentation}`;
@@ -13159,16 +13233,16 @@ ${originalIndentation}`;
13159
13233
  return '"[Object]"';
13160
13234
  }
13161
13235
  indentation += spacer;
13162
- const join7 = `,
13236
+ const join8 = `,
13163
13237
  ${indentation}`;
13164
13238
  let res = "";
13165
13239
  let separator = "";
13166
13240
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
13167
13241
  if (isTypedArrayWithEntries(value)) {
13168
- res += stringifyTypedArray(value, join7, maximumBreadth);
13242
+ res += stringifyTypedArray(value, join8, maximumBreadth);
13169
13243
  keys = keys.slice(value.length);
13170
13244
  maximumPropertiesToStringify -= value.length;
13171
- separator = join7;
13245
+ separator = join8;
13172
13246
  }
13173
13247
  if (deterministic) {
13174
13248
  keys = sort(keys, comparator);
@@ -13179,13 +13253,13 @@ ${indentation}`;
13179
13253
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
13180
13254
  if (tmp !== void 0) {
13181
13255
  res += `${separator}${strEscape(key2)}: ${tmp}`;
13182
- separator = join7;
13256
+ separator = join8;
13183
13257
  }
13184
13258
  }
13185
13259
  if (keyLength > maximumBreadth) {
13186
13260
  const removedKeys = keyLength - maximumBreadth;
13187
13261
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
13188
- separator = join7;
13262
+ separator = join8;
13189
13263
  }
13190
13264
  if (separator !== "") {
13191
13265
  res = `
@@ -61861,11 +61935,11 @@ var require_core = __commonJS({
61861
61935
  function mapOuter(r, f) {
61862
61936
  return r.matched ? f(r) : r;
61863
61937
  }
61864
- function ab(pa, pb, join7) {
61865
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join7(ma.value, vb, data, i, j)));
61938
+ function ab(pa, pb, join8) {
61939
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join8(ma.value, vb, data, i, j)));
61866
61940
  }
61867
- function abc(pa, pb, pc, join7) {
61868
- 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))));
61941
+ function abc(pa, pb, pc, join8) {
61942
+ 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))));
61869
61943
  }
61870
61944
  function action(f) {
61871
61945
  return (data, i) => {
@@ -78402,6 +78476,10 @@ var require_discover_ai_planner = __commonJS({
78402
78476
  if (context.appBrief) {
78403
78477
  sections.push(`## Application Brief
78404
78478
  ${context.appBrief.slice(0, 500)}`);
78479
+ }
78480
+ if (context.knowledgeContext && context.knowledgeContext.trim().length > 0) {
78481
+ sections.push(`## Known Facts
78482
+ ${context.knowledgeContext.slice(0, 4e3)}`);
78405
78483
  }
78406
78484
  sections.push(`## Base URL
78407
78485
  ${compressed.baseUrl}`);
@@ -83446,48 +83524,29 @@ ${statePacket}`, redactionCredentials) },
83446
83524
  log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
83447
83525
  return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
83448
83526
  }
83449
- if (platform3 !== "IOS") {
83450
- const appDataCleared = await driver.resetAppState?.().catch((err) => {
83451
- log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
83452
- return false;
83453
- }) ?? false;
83454
- if (appDataCleared) {
83455
- return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
83456
- }
83457
- await driver.relaunch();
83458
- if (resetPolicy === "CLEAR_APP_DATA") {
83459
- log2(" CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
83460
- return {
83461
- appDataCleared: false,
83462
- coldResetUnavailable: true,
83463
- refuseGenerate: true,
83464
- reason: "clear_app_data_unavailable"
83465
- };
83466
- }
83467
- log2(" app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
83468
- return applyScenarioEntryDeepLink(driver, target, {
83469
- appDataCleared: false,
83470
- coldResetUnavailable: true,
83471
- refuseGenerate: false,
83472
- reason: platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
83473
- }, log2);
83527
+ const appDataCleared = await driver.resetAppState?.().catch((err) => {
83528
+ log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
83529
+ return false;
83530
+ }) ?? false;
83531
+ if (appDataCleared) {
83532
+ return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
83474
83533
  }
83475
83534
  await driver.relaunch();
83476
83535
  if (resetPolicy === "CLEAR_APP_DATA") {
83477
- log2(" CLEAR_APP_DATA required but iOS has no supported clearApp primitive \u2014 refusing to generate from warm state.");
83536
+ 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.");
83478
83537
  return {
83479
83538
  appDataCleared: false,
83480
83539
  coldResetUnavailable: true,
83481
83540
  refuseGenerate: true,
83482
- reason: "ios_clear_app_data_unavailable"
83541
+ reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
83483
83542
  };
83484
83543
  }
83485
- log2(" iOS has no clearApp \u2014 scenario records after relaunch (warm-state risk for auth flows).");
83544
+ 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).");
83486
83545
  return applyScenarioEntryDeepLink(driver, target, {
83487
83546
  appDataCleared: false,
83488
83547
  coldResetUnavailable: true,
83489
83548
  refuseGenerate: false,
83490
- reason: "ios_no_clear_app"
83549
+ reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
83491
83550
  }, log2);
83492
83551
  }
83493
83552
  async function runMobileGeneratePhase(args) {
@@ -88948,7 +89007,7 @@ var require_kill_tree = __commonJS({
88948
89007
  var node_child_process_1 = require("child_process");
88949
89008
  var node_util_1 = require("util");
88950
89009
  var node_fs_1 = require("fs");
88951
- var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
89010
+ var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
88952
89011
  var PS_TIMEOUT_MS = 2e3;
88953
89012
  var PS_BUFFER = 1024 * 1024;
88954
89013
  async function gracefulCloseWithTreeKill(options) {
@@ -89002,7 +89061,7 @@ var require_kill_tree = __commonJS({
89002
89061
  timer.unref?.();
89003
89062
  let stdout;
89004
89063
  try {
89005
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
89064
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
89006
89065
  maxBuffer: PS_BUFFER,
89007
89066
  signal: controller.signal
89008
89067
  });
@@ -89040,7 +89099,7 @@ var require_kill_tree = __commonJS({
89040
89099
  timer.unref?.();
89041
89100
  let stdout;
89042
89101
  try {
89043
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
89102
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
89044
89103
  maxBuffer: PS_BUFFER,
89045
89104
  signal: controller.signal
89046
89105
  });
@@ -89117,7 +89176,7 @@ var require_kill_tree = __commonJS({
89117
89176
  timer.unref?.();
89118
89177
  let stdout;
89119
89178
  try {
89120
- const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
89179
+ const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
89121
89180
  maxBuffer: PS_BUFFER,
89122
89181
  signal: controller.signal
89123
89182
  });
@@ -92283,6 +92342,15 @@ var require_agentic_discovery_prompts = __commonJS({
92283
92342
  lines.push(input.appBrief.trim());
92284
92343
  lines.push("");
92285
92344
  }
92345
+ if (input.knownFactsBlock && input.knownFactsBlock.trim()) {
92346
+ lines.push("## Known facts about this feature (from project documentation)");
92347
+ lines.push("");
92348
+ lines.push("Advisory hints about routes/data/constraints \u2014 verify against the live");
92349
+ lines.push("app; the browser is the source of truth for locators.");
92350
+ lines.push("");
92351
+ lines.push(input.knownFactsBlock.trim());
92352
+ lines.push("");
92353
+ }
92286
92354
  if (input.credentials && input.credentials.length > 0) {
92287
92355
  lines.push("## Test credentials (throwaway accounts)");
92288
92356
  lines.push("");
@@ -98658,8 +98726,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
98658
98726
  onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
98659
98727
  });
98660
98728
  } catch (aiErr) {
98661
- const errMsg2 = aiErr instanceof Error ? aiErr.message : String(aiErr);
98662
- logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg2}`);
98729
+ const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
98730
+ logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
98663
98731
  throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
98664
98732
  }
98665
98733
  const iterDuration = Date.now() - iterStart;
@@ -98702,8 +98770,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
98702
98770
  try {
98703
98771
  toolArgs = JSON.parse(toolCall.function.arguments);
98704
98772
  } catch (parseErr) {
98705
- const errMsg2 = `Invalid tool arguments: ${parseErr.message}`;
98706
- toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg2 });
98773
+ const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
98774
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
98707
98775
  continue;
98708
98776
  }
98709
98777
  if (toolName === "finish_generate") {
@@ -99921,7 +99989,7 @@ var require_util4 = __commonJS({
99921
99989
  }
99922
99990
  path = url.path;
99923
99991
  }
99924
- var isAbsolute = exports2.isAbsolute(path);
99992
+ var isAbsolute2 = exports2.isAbsolute(path);
99925
99993
  var parts = path.split(/\/+/);
99926
99994
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
99927
99995
  part = parts[i];
@@ -99941,7 +100009,7 @@ var require_util4 = __commonJS({
99941
100009
  }
99942
100010
  path = parts.join("/");
99943
100011
  if (path === "") {
99944
- path = isAbsolute ? "/" : ".";
100012
+ path = isAbsolute2 ? "/" : ".";
99945
100013
  }
99946
100014
  if (url) {
99947
100015
  url.path = path;
@@ -99950,7 +100018,7 @@ var require_util4 = __commonJS({
99950
100018
  return path;
99951
100019
  }
99952
100020
  exports2.normalize = normalize;
99953
- function join7(aRoot, aPath) {
100021
+ function join8(aRoot, aPath) {
99954
100022
  if (aRoot === "") {
99955
100023
  aRoot = ".";
99956
100024
  }
@@ -99982,7 +100050,7 @@ var require_util4 = __commonJS({
99982
100050
  }
99983
100051
  return joined;
99984
100052
  }
99985
- exports2.join = join7;
100053
+ exports2.join = join8;
99986
100054
  exports2.isAbsolute = function(aPath) {
99987
100055
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
99988
100056
  };
@@ -100155,7 +100223,7 @@ var require_util4 = __commonJS({
100155
100223
  parsed.path = parsed.path.substring(0, index + 1);
100156
100224
  }
100157
100225
  }
100158
- sourceURL = join7(urlGenerate(parsed), sourceURL);
100226
+ sourceURL = join8(urlGenerate(parsed), sourceURL);
100159
100227
  }
100160
100228
  return normalize(sourceURL);
100161
100229
  }
@@ -313867,8 +313935,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
313867
313935
  ...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
313868
313936
  })), { label: `script-heal-${attempt}` });
313869
313937
  } catch (aiErr) {
313870
- const errMsg2 = aiErr instanceof Error ? aiErr.message : String(aiErr);
313871
- logs2.push(`\u26A0 AI call failed: ${errMsg2}`);
313938
+ const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
313939
+ logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
313872
313940
  attemptHistory.push({
313873
313941
  phase: "script",
313874
313942
  attempt,
@@ -313876,13 +313944,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
313876
313944
  passed: false,
313877
313945
  changedCode: false,
313878
313946
  summary: "AI call failed before returning a heal result",
313879
- errorSummary: truncateForPrompt(errMsg2),
313947
+ errorSummary: truncateForPrompt(errMsg3),
313880
313948
  failedStep: summarizeFailedStep(currentStepResults)
313881
313949
  });
313882
313950
  if (feedbackEnabled) {
313883
- lastTransportFailure = truncateForPrompt(errMsg2, 200) ?? "the AI request failed";
313951
+ lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
313884
313952
  } else {
313885
- currentError = `AI call failed on attempt ${attempt}: ${errMsg2}`;
313953
+ currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
313886
313954
  }
313887
313955
  markRecoverableTurn();
313888
313956
  continue;
@@ -314132,8 +314200,8 @@ ${currentError.slice(0, 2e3)}`
314132
314200
  // item 5b
314133
314201
  });
314134
314202
  } catch (runErr) {
314135
- const errMsg2 = runErr instanceof Error ? runErr.message : String(runErr);
314136
- logs2.push(`\u{1F4A5} Native execution threw: ${errMsg2}`);
314203
+ const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
314204
+ logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
314137
314205
  attemptHistory.push({
314138
314206
  phase: "script",
314139
314207
  attempt,
@@ -314141,9 +314209,9 @@ ${currentError.slice(0, 2e3)}`
314141
314209
  passed: false,
314142
314210
  changedCode: true,
314143
314211
  summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
314144
- errorSummary: truncateForPrompt(errMsg2)
314212
+ errorSummary: truncateForPrompt(errMsg3)
314145
314213
  });
314146
- currentError = `Native execution threw: ${errMsg2}`;
314214
+ currentError = `Native execution threw: ${errMsg3}`;
314147
314215
  currentStepResults = [];
314148
314216
  continue;
314149
314217
  }
@@ -314899,9 +314967,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
314899
314967
  previousErrorSignature = errorSig;
314900
314968
  }
314901
314969
  } catch (runErr) {
314902
- const errMsg2 = runErr instanceof Error ? runErr.message : String(runErr);
314903
- logs2.push(`\u{1F4A5} Native run threw: ${errMsg2}`);
314904
- currentError = errMsg2;
314970
+ const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
314971
+ logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
314972
+ currentError = errMsg3;
314905
314973
  currentStepResults = [];
314906
314974
  }
314907
314975
  } else {
@@ -315463,8 +315531,8 @@ When you call finish_fix, preserve locators only when they are stable structural
315463
315531
  }
315464
315532
  }
315465
315533
  } catch (toolErr) {
315466
- const errMsg2 = toolErr instanceof Error ? toolErr.message : String(toolErr);
315467
- let errorContent = `Error: ${errMsg2}`;
315534
+ const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
315535
+ let errorContent = `Error: ${errMsg3}`;
315468
315536
  try {
315469
315537
  const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
315470
315538
  const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
@@ -315485,8 +315553,8 @@ ${errorContent}`;
315485
315553
  }
315486
315554
  }
315487
315555
  } catch (mcpErr) {
315488
- const errMsg2 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
315489
- logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg2}`);
315556
+ const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
315557
+ logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
315490
315558
  }
315491
315559
  if (verdict === "app_bug") {
315492
315560
  phase2AttemptHistory.push({
@@ -316049,9 +316117,9 @@ ${testBlocks}
316049
316117
  }
316050
316118
  emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
316051
316119
  } catch (err) {
316052
- const errMsg2 = err instanceof Error ? err.message : String(err);
316053
- toolResultContent = `ERROR: ${errMsg2}`;
316054
- emit(` \u2717 ${errMsg2}`);
316120
+ const errMsg3 = err instanceof Error ? err.message : String(err);
316121
+ toolResultContent = `ERROR: ${errMsg3}`;
316122
+ emit(` \u2717 ${errMsg3}`);
316055
316123
  }
316056
316124
  toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
316057
316125
  }
@@ -321906,8 +321974,8 @@ var require_capture_page_normalizer = __commonJS({
321906
321974
  const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
321907
321975
  snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
321908
321976
  } catch (err) {
321909
- const errMsg2 = err instanceof Error ? err.message : String(err);
321910
- const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg2);
321977
+ const errMsg3 = err instanceof Error ? err.message : String(err);
321978
+ const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
321911
321979
  return {
321912
321980
  snapshotText: null,
321913
321981
  meta: {
@@ -325929,9 +325997,9 @@ ${currentSummary}`;
325929
325997
  });
325930
325998
  }
325931
325999
  } catch (err) {
325932
- const errMsg2 = err instanceof Error ? err.message : String(err);
325933
- logs2.push(` Error persisting page: ${errMsg2}`);
325934
- toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg2}` });
326000
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326001
+ logs2.push(` Error persisting page: ${errMsg3}`);
326002
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
325935
326003
  }
325936
326004
  continue;
325937
326005
  }
@@ -326133,12 +326201,12 @@ ${currentSummary}`;
326133
326201
  })
326134
326202
  });
326135
326203
  } catch (err) {
326136
- const errMsg2 = err instanceof Error ? err.message : String(err);
326137
- logs2.push(` Error persisting page: ${errMsg2}`);
326204
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326205
+ logs2.push(` Error persisting page: ${errMsg3}`);
326138
326206
  toolResults.push({
326139
326207
  role: "tool",
326140
326208
  tool_call_id: toolCall.id,
326141
- content: `ERROR persisting page: ${errMsg2}`
326209
+ content: `ERROR persisting page: ${errMsg3}`
326142
326210
  });
326143
326211
  }
326144
326212
  continue;
@@ -326173,12 +326241,12 @@ ${currentSummary}`;
326173
326241
  content: JSON.stringify({ acknowledged: true })
326174
326242
  });
326175
326243
  } catch (err) {
326176
- const errMsg2 = err instanceof Error ? err.message : String(err);
326177
- logs2.push(` Error persisting transition: ${errMsg2}`);
326244
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326245
+ logs2.push(` Error persisting transition: ${errMsg3}`);
326178
326246
  toolResults.push({
326179
326247
  role: "tool",
326180
326248
  tool_call_id: toolCall.id,
326181
- content: `ERROR persisting transition: ${errMsg2}`
326249
+ content: `ERROR persisting transition: ${errMsg3}`
326182
326250
  });
326183
326251
  }
326184
326252
  continue;
@@ -326833,15 +326901,15 @@ Exploration complete: ${summary}`);
326833
326901
  toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
326834
326902
  logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
326835
326903
  } catch (err) {
326836
- const errMsg2 = err instanceof Error ? err.message : String(err);
326837
- toolResultContent = `ERROR: ${errMsg2}`;
326838
- logs2.push(` Error: ${errMsg2}`);
326904
+ const errMsg3 = err instanceof Error ? err.message : String(err);
326905
+ toolResultContent = `ERROR: ${errMsg3}`;
326906
+ logs2.push(` Error: ${errMsg3}`);
326839
326907
  if (useV2) {
326840
326908
  (0, exploration_state_js_1.reduceEvent)(state2, {
326841
326909
  type: "ACTION_FAILED",
326842
326910
  route: state2.currentRoute,
326843
326911
  action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
326844
- error: errMsg2
326912
+ error: errMsg3
326845
326913
  });
326846
326914
  }
326847
326915
  }
@@ -328950,6 +329018,15 @@ var require_agentic_survey_prompts = __commonJS({
328950
329018
  lines.push(input.appBrief.trim());
328951
329019
  lines.push("");
328952
329020
  }
329021
+ if (input.knownFactsBlock && input.knownFactsBlock.trim().length > 0) {
329022
+ lines.push("## Known facts about this app (from project documentation)");
329023
+ lines.push("");
329024
+ lines.push("Use these as hints for what features/routes/constraints exist. They are");
329025
+ lines.push("advisory \u2014 verify against the live app; the browser is the source of truth.");
329026
+ lines.push("");
329027
+ lines.push(input.knownFactsBlock.trim());
329028
+ lines.push("");
329029
+ }
328953
329030
  if (input.credentials && input.credentials.length > 0) {
328954
329031
  lines.push("## Test credentials (throwaway accounts)");
328955
329032
  lines.push("");
@@ -329577,6 +329654,7 @@ var require_grok_survey_runner = __commonJS({
329577
329654
  const briefInput = {
329578
329655
  baseUrl: ctx.baseUrl,
329579
329656
  appBrief: ctx.appBrief,
329657
+ knownFactsBlock: ctx.knowledgeContext,
329580
329658
  credentials: credentials.length > 0 ? credentials : void 0,
329581
329659
  existingFeatureNames: ctx.existingFeatureNames ?? [],
329582
329660
  focusAreas: ctx.focusAreas,
@@ -329677,10 +329755,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
329677
329755
  onLog: options.onLog,
329678
329756
  onProvenCommands: recordProvenCommands
329679
329757
  });
329680
- const errMsg2 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
329758
+ const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
329681
329759
  const fixPrompt = [
329682
329760
  "Your previous attempt did not produce a valid result file.",
329683
- `Error: ${errMsg2}`,
329761
+ `Error: ${errMsg3}`,
329684
329762
  `Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
329685
329763
  "Single JSON object. No prose. Then exit."
329686
329764
  ].join("\n");
@@ -331272,6 +331350,7 @@ var require_grok_discovery_runner = __commonJS({
331272
331350
  featureObservations: ctx.featureContext?.flowIntel,
331273
331351
  notesForTestAuthor: ctx.featureContext?.priorInsight,
331274
331352
  appBrief: ctx.appBrief,
331353
+ knownFactsBlock: ctx.knowledgeContext,
331275
331354
  credentials: credentials.length > 0 ? credentials : void 0,
331276
331355
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
331277
331356
  existingTestNames: ctx.existingTestNames ?? [],
@@ -331820,7 +331899,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
331820
331899
  log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
331821
331900
  }
331822
331901
  if (!parsedResult) {
331823
- const errMsg2 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
331902
+ const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
331824
331903
  if (planDebounceTimerRef.current) {
331825
331904
  clearTimeout(planDebounceTimerRef.current);
331826
331905
  planDebounceTimerRef.current = void 0;
@@ -331845,7 +331924,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
331845
331924
  ""
331846
331925
  ].join("\n");
331847
331926
  const fixPrompt = [
331848
- `Your previous attempt did not produce a valid result file. Error: ${errMsg2}`,
331927
+ `Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
331849
331928
  "Re-read BRIEF.md.",
331850
331929
  phaseContext,
331851
331930
  `Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
@@ -332209,6 +332288,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
332209
332288
  featureObservations: ctx.featureContext?.flowIntel,
332210
332289
  notesForTestAuthor: ctx.featureContext?.priorInsight,
332211
332290
  appBrief: ctx.appBrief,
332291
+ knownFactsBlock: ctx.knowledgeContext,
332212
332292
  credentials: credentials.length > 0 ? credentials : void 0,
332213
332293
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
332214
332294
  existingTestNames: ctx.existingTestNames ?? [],
@@ -333300,6 +333380,7 @@ ${preview}`);
333300
333380
  const briefInput = {
333301
333381
  baseUrl: ctx.baseUrl,
333302
333382
  appBrief: ctx.appBrief,
333383
+ knownFactsBlock: ctx.knowledgeContext,
333303
333384
  credentials: credentials.length > 0 ? credentials : void 0,
333304
333385
  existingFeatureNames: ctx.existingFeatureNames ?? [],
333305
333386
  focusAreas: ctx.focusAreas,
@@ -333472,10 +333553,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
333472
333553
  onLog: parserLog,
333473
333554
  onProvenCommands: recordProvenCommands
333474
333555
  });
333475
- const errMsg2 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
333556
+ const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
333476
333557
  const fixPrompt = [
333477
333558
  "Your previous attempt did not leave a valid result file.",
333478
- `Error: ${errMsg2}`,
333559
+ `Error: ${errMsg3}`,
333479
333560
  `Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
333480
333561
  "Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
333481
333562
  `the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
@@ -334159,6 +334240,7 @@ var require_claude_discovery_runner = __commonJS({
334159
334240
  featureObservations: ctx.featureContext?.flowIntel,
334160
334241
  notesForTestAuthor: ctx.featureContext?.priorInsight,
334161
334242
  appBrief: ctx.appBrief,
334243
+ knownFactsBlock: ctx.knowledgeContext,
334162
334244
  credentials: credentials.length > 0 ? credentials : void 0,
334163
334245
  testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
334164
334246
  existingTestNames: ctx.existingTestNames ?? [],
@@ -336013,7 +336095,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
336013
336095
  // scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
336014
336096
  // auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
336015
336097
  emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
336016
- testBudget
336098
+ testBudget,
336099
+ knowledgeContext: ctx.knowledgeContext
336017
336100
  }, {
336018
336101
  modelOverride: ctx.planModel ?? ctx.generateModel,
336019
336102
  gateModelOverride: ctx.planGateModel,
@@ -336125,6 +336208,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
336125
336208
  interactionLog: exploreResult.interactionLog,
336126
336209
  modelOverride: ctx.generateModel,
336127
336210
  appBrief: ctx.appBrief,
336211
+ knowledgeContext: ctx.knowledgeContext,
336128
336212
  onAICall,
336129
336213
  onProgress,
336130
336214
  mcpClient: mcpClient ?? void 0,
@@ -336604,7 +336688,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
336604
336688
  // configured inbox), so the planner never promises what generation can't do.
336605
336689
  // Scoped to AUTH features: a non-auth feature has no signup surface to test.
336606
336690
  emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
336607
- testBudget: featureTestBudget
336691
+ testBudget: featureTestBudget,
336692
+ knowledgeContext: ctx.knowledgeContext
336608
336693
  }, {
336609
336694
  modelOverride: ctx.planModel ?? ctx.generateModel,
336610
336695
  gateModelOverride: ctx.planGateModel,
@@ -336650,6 +336735,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
336650
336735
  interactionLog: exploreResult.interactionLog,
336651
336736
  modelOverride: ctx.generateModel,
336652
336737
  appBrief: ctx.appBrief,
336738
+ knowledgeContext: ctx.knowledgeContext,
336653
336739
  onAICall,
336654
336740
  mcpClient: mcpClient ?? void 0,
336655
336741
  // Pooled browser for parallel scenario generation (Fargate opt-in via
@@ -339848,8 +339934,8 @@ var require_adm_zip = __commonJS({
339848
339934
  return null;
339849
339935
  }
339850
339936
  function fixPath(zipPath) {
339851
- const { join: join7, normalize, sep } = pth.posix;
339852
- return join7(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
339937
+ const { join: join8, normalize, sep } = pth.posix;
339938
+ return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
339853
339939
  }
339854
339940
  function filenameFilter(filterfn) {
339855
339941
  if (filterfn instanceof RegExp) {
@@ -342684,11 +342770,11 @@ var require_winchars = __commonJS({
342684
342770
  var require_strip_absolute_path = __commonJS({
342685
342771
  "../../node_modules/tar/lib/strip-absolute-path.js"(exports2, module2) {
342686
342772
  "use strict";
342687
- var { isAbsolute, parse } = require("path").win32;
342773
+ var { isAbsolute: isAbsolute2, parse } = require("path").win32;
342688
342774
  module2.exports = (path) => {
342689
342775
  let r = "";
342690
342776
  let parsed = parse(path);
342691
- while (isAbsolute(path) || parsed.root) {
342777
+ while (isAbsolute2(path) || parsed.root) {
342692
342778
  const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
342693
342779
  path = path.slice(root.length);
342694
342780
  r += root;
@@ -345200,7 +345286,7 @@ var require_update = __commonJS({
345200
345286
  var require_opts_arg = __commonJS({
345201
345287
  "../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
345202
345288
  "use strict";
345203
- var { promisify: promisify3 } = require("util");
345289
+ var { promisify: promisify4 } = require("util");
345204
345290
  var fs = require("fs");
345205
345291
  var optsArg = (opts) => {
345206
345292
  if (!opts)
@@ -345214,9 +345300,9 @@ var require_opts_arg = __commonJS({
345214
345300
  else
345215
345301
  throw new TypeError("invalid options argument");
345216
345302
  opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
345217
- opts.mkdirAsync = promisify3(opts.mkdir);
345303
+ opts.mkdirAsync = promisify4(opts.mkdir);
345218
345304
  opts.stat = opts.stat || opts.fs.stat || fs.stat;
345219
- opts.statAsync = promisify3(opts.stat);
345305
+ opts.statAsync = promisify4(opts.stat);
345220
345306
  opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
345221
345307
  opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
345222
345308
  return opts;
@@ -345469,10 +345555,10 @@ var require_chownr = __commonJS({
345469
345555
  }
345470
345556
  } : (path2, uid, gid) => lchownSync(path2, uid, gid);
345471
345557
  var nodeVersion = process.version;
345472
- var readdir2 = (path2, options, cb) => fs.readdir(path2, options, cb);
345558
+ var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
345473
345559
  var readdirSync = (path2, options) => fs.readdirSync(path2, options);
345474
345560
  if (/^v4\./.test(nodeVersion))
345475
- readdir2 = (path2, options, cb) => fs.readdir(path2, cb);
345561
+ readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
345476
345562
  var chown = (cpath, uid, gid, cb) => {
345477
345563
  fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
345478
345564
  cb(er && er.code !== "ENOENT" ? er : null);
@@ -345499,7 +345585,7 @@ var require_chownr = __commonJS({
345499
345585
  }
345500
345586
  };
345501
345587
  var chownr = (p, uid, gid, cb) => {
345502
- readdir2(p, { withFileTypes: true }, (er, children) => {
345588
+ readdir3(p, { withFileTypes: true }, (er, children) => {
345503
345589
  if (er) {
345504
345590
  if (er.code === "ENOENT")
345505
345591
  return cb();
@@ -345773,7 +345859,7 @@ var require_path_reservations = __commonJS({
345773
345859
  var assert = require("assert");
345774
345860
  var normalize = require_normalize_unicode();
345775
345861
  var stripSlashes = require_strip_trailing_slashes();
345776
- var { join: join7 } = require("path");
345862
+ var { join: join8 } = require("path");
345777
345863
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
345778
345864
  var isWindows = platform3 === "win32";
345779
345865
  module2.exports = () => {
@@ -345782,7 +345868,7 @@ var require_path_reservations = __commonJS({
345782
345868
  const getDirs = (path) => {
345783
345869
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
345784
345870
  if (set.length) {
345785
- path2 = join7(set[set.length - 1], path2);
345871
+ path2 = join8(set[set.length - 1], path2);
345786
345872
  }
345787
345873
  set.push(path2 || "/");
345788
345874
  return set;
@@ -345850,7 +345936,7 @@ var require_path_reservations = __commonJS({
345850
345936
  };
345851
345937
  const reserve = (paths, fn) => {
345852
345938
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
345853
- return stripSlashes(join7(normalize(p))).toLowerCase();
345939
+ return stripSlashes(join8(normalize(p))).toLowerCase();
345854
345940
  });
345855
345941
  const dirs = new Set(
345856
345942
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -347863,6 +347949,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347863
347949
  "Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
347864
347950
  "assertVisible or assertText unless tagging @needs-review. When an incremental",
347865
347951
  "allow-list is present, extract ONLY from those files.",
347952
+ "",
347953
+ "Every tap/type/clear/waitForElement/assertVisible/assertText target MUST",
347954
+ "carry an IDENTITY locator: ~accessibility-id, id=resource-id, visible text,",
347955
+ "or an attribute-EQUALITY xpath (//android.widget.Button[@text='Submit']).",
347956
+ "Positional/index-only xpaths (/hierarchy/\u2026/View[3]) are rejected \u2014 imported",
347957
+ "steps have no recorded coordinates, so such a step can never replay.",
347866
347958
  "Write strictly valid JSON."
347867
347959
  ].join("\n");
347868
347960
  }
@@ -347880,6 +347972,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347880
347972
  function isPlainObject2(value) {
347881
347973
  return !!value && typeof value === "object" && !Array.isArray(value);
347882
347974
  }
347975
+ var LOCATOR_REQUIRING_IMPORT_ACTIONS = /* @__PURE__ */ new Set([
347976
+ "tap",
347977
+ "type",
347978
+ "clear",
347979
+ "waitForElement",
347980
+ "assertVisible",
347981
+ "assertText"
347982
+ ]);
347983
+ function selectorValueHasIdentity(value) {
347984
+ const trimmed = value.trim();
347985
+ if (!trimmed)
347986
+ return false;
347987
+ if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
347988
+ return (0, shared_1.isIdentityMobileXpath)(trimmed);
347989
+ return true;
347990
+ }
347991
+ function stepLocatorLacksIdentity(step) {
347992
+ if (!LOCATOR_REQUIRING_IMPORT_ACTIONS.has(step.action))
347993
+ return false;
347994
+ const target = step.target;
347995
+ if (!target)
347996
+ return true;
347997
+ if ((0, shared_1.mobileLocatorBundleHasIdentity)(target.locatorBundle))
347998
+ return false;
347999
+ return ![target.primary, ...target.fallbacks ?? []].some((value) => typeof value === "string" && selectorValueHasIdentity(value));
348000
+ }
347883
348001
  function isValidStep(value) {
347884
348002
  if (!value || typeof value !== "object" || Array.isArray(value))
347885
348003
  return false;
@@ -347931,6 +348049,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
347931
348049
  if (!o.steps.every(isValidStep)) {
347932
348050
  return { ok: false, reason: "steps: invalid step shape" };
347933
348051
  }
348052
+ const identityless = o.steps.find(stepLocatorLacksIdentity);
348053
+ if (identityless) {
348054
+ return {
348055
+ ok: false,
348056
+ 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'].`
348057
+ };
348058
+ }
347934
348059
  const steps = o.steps.map((step, order) => ({
347935
348060
  ...step,
347936
348061
  order,
@@ -350026,7 +350151,7 @@ var require_process_inventory = __commonJS({
350026
350151
  var node_child_process_1 = require("child_process");
350027
350152
  var node_os_1 = __importDefault(require("os"));
350028
350153
  var node_util_1 = require("util");
350029
- var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
350154
+ var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
350030
350155
  var DEFAULT_LIMIT = 60;
350031
350156
  var DEFAULT_TIMEOUT_MS2 = 800;
350032
350157
  var MAX_COMMAND_LENGTH = 320;
@@ -350088,7 +350213,7 @@ var require_process_inventory = __commonJS({
350088
350213
  const timer = setTimeout(() => controller.abort(), timeoutMs);
350089
350214
  timer.unref?.();
350090
350215
  try {
350091
- const { stdout } = await execFileAsync3(command, args, {
350216
+ const { stdout } = await execFileAsync4(command, args, {
350092
350217
  maxBuffer: 1024 * 1024,
350093
350218
  signal: controller.signal
350094
350219
  });
@@ -351224,7 +351349,7 @@ var require_package4 = __commonJS({
351224
351349
  "package.json"(exports2, module2) {
351225
351350
  module2.exports = {
351226
351351
  name: "@validate.qa/runner",
351227
- version: "1.0.67",
351352
+ version: "1.0.69",
351228
351353
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
351229
351354
  bin: {
351230
351355
  "validate-runner": "dist/cli.js"
@@ -351311,7 +351436,7 @@ function getConfigDir() {
351311
351436
  }
351312
351437
 
351313
351438
  // src/runner.ts
351314
- var import_path4 = require("path");
351439
+ var import_path5 = require("path");
351315
351440
  var import_os6 = require("os");
351316
351441
  var import_node_crypto2 = require("crypto");
351317
351442
 
@@ -351906,6 +352031,66 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
351906
352031
  };
351907
352032
  }
351908
352033
 
352034
+ // src/services/mobile/ios-simulator-reset.ts
352035
+ var import_child_process2 = require("child_process");
352036
+ var import_util = require("util");
352037
+ var import_promises = require("fs/promises");
352038
+ var import_path3 = require("path");
352039
+ var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
352040
+ var SIMCTL_TIMEOUT_MS = 15e3;
352041
+ 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;
352042
+ function isIosSimulatorUdid(udid) {
352043
+ return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
352044
+ }
352045
+ function errMsg(err) {
352046
+ return err instanceof Error ? err.message : String(err);
352047
+ }
352048
+ async function simctl(args) {
352049
+ const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
352050
+ timeout: SIMCTL_TIMEOUT_MS,
352051
+ encoding: "utf-8"
352052
+ });
352053
+ return stdout.trim();
352054
+ }
352055
+ async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
352056
+ if (!isIosSimulatorUdid(udid)) {
352057
+ return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
352058
+ }
352059
+ let container;
352060
+ try {
352061
+ container = await simctl(["get_app_container", udid, bundleId, "data"]);
352062
+ } catch (err) {
352063
+ return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
352064
+ }
352065
+ if (!(0, import_path3.isAbsolute)(container) || !container.includes("/Containers/Data/Application/")) {
352066
+ return { ok: false, detail: `unexpected app container path: ${container}` };
352067
+ }
352068
+ try {
352069
+ await simctl(["spawn", udid, "defaults", "delete", bundleId]);
352070
+ } catch {
352071
+ }
352072
+ try {
352073
+ const entries = await (0, import_promises.readdir)(container);
352074
+ for (const entry of entries) {
352075
+ await (0, import_promises.rm)((0, import_path3.join)(container, entry), { recursive: true, force: true });
352076
+ }
352077
+ } catch (err) {
352078
+ return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
352079
+ }
352080
+ const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
352081
+ if (keychainPolicy !== "never") {
352082
+ try {
352083
+ await simctl(["keychain", udid, "reset"]);
352084
+ } catch (err) {
352085
+ return {
352086
+ ok: false,
352087
+ 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.`
352088
+ };
352089
+ }
352090
+ }
352091
+ return { ok: true };
352092
+ }
352093
+
351909
352094
  // src/services/mobile/appium-actions.ts
351910
352095
  var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
351911
352096
  var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
@@ -352088,9 +352273,12 @@ async function tapAt(sessionPath, bounds) {
352088
352273
  }
352089
352274
  }
352090
352275
  function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
352091
- if (platform3 !== "ANDROID") return false;
352092
352276
  const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
352093
352277
  if (policy === "never") return false;
352278
+ if (platform3 === "IOS") {
352279
+ return isIosSimulatorUdid(deviceId);
352280
+ }
352281
+ if (platform3 !== "ANDROID") return false;
352094
352282
  if (policy === "always") return true;
352095
352283
  return !!deviceId && deviceId.startsWith("emulator-");
352096
352284
  }
@@ -352977,11 +353165,11 @@ var import_node_path2 = require("path");
352977
353165
  var import_runner_core8 = __toESM(require_dist2());
352978
353166
 
352979
353167
  // src/services/device-discovery.ts
352980
- var import_child_process2 = require("child_process");
353168
+ var import_child_process3 = require("child_process");
352981
353169
  var import_os3 = require("os");
352982
353170
  function runCommand(command, args, timeout = 1e4) {
352983
353171
  try {
352984
- return (0, import_child_process2.execFileSync)(command, args, {
353172
+ return (0, import_child_process3.execFileSync)(command, args, {
352985
353173
  encoding: "utf-8",
352986
353174
  timeout,
352987
353175
  stdio: ["pipe", "pipe", "pipe"]
@@ -353281,9 +353469,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
353281
353469
  function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
353282
353470
  const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
353283
353471
  if (ready.length === 0) return { kind: "busy", reason: `no booted ${platform3} device` };
353284
- if (requirements.resetPolicy === "CLEAR_APP_DATA" && platform3 !== "ANDROID") {
353285
- return { kind: "unsatisfiable", reason: "CLEAR_APP_DATA is unsupported on iOS" };
353286
- }
353287
353472
  let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
353288
353473
  const appId = requirements.appId?.trim();
353289
353474
  if (appId) {
@@ -353333,13 +353518,13 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
353333
353518
  // src/services/mobile/mobile-network-capture.ts
353334
353519
  var import_node_child_process = require("child_process");
353335
353520
  var import_node_crypto = require("crypto");
353336
- var import_promises = require("fs/promises");
353521
+ var import_promises2 = require("fs/promises");
353337
353522
  var import_node_os = require("os");
353338
353523
  var import_node_path = require("path");
353339
353524
  var import_node_util = require("util");
353340
353525
  var import_mockttp = require("mockttp");
353341
353526
  var import_runner_core7 = __toESM(require_dist2());
353342
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
353527
+ var execFileAsync2 = (0, import_node_util.promisify)(import_node_child_process.execFile);
353343
353528
  var MAX_CALLS = 1e3;
353344
353529
  var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
353345
353530
  var DEVICE_CMD_TIMEOUT_MS = 15e3;
@@ -353527,7 +353712,7 @@ var MitmProxy = class {
353527
353712
  try {
353528
353713
  await server3.stop();
353529
353714
  } catch (err) {
353530
- this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg(err)}`);
353715
+ this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
353531
353716
  }
353532
353717
  this.onLog?.(
353533
353718
  `[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
@@ -353552,7 +353737,7 @@ var MitmProxy = class {
353552
353737
  isHttps
353553
353738
  });
353554
353739
  } catch (err) {
353555
- this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg(err)}`);
353740
+ this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
353556
353741
  }
353557
353742
  }
353558
353743
  // ── response → finalize the matched request ───────────────────────────────
@@ -353595,7 +353780,7 @@ var MitmProxy = class {
353595
353780
  intercepted: true
353596
353781
  });
353597
353782
  } catch (err) {
353598
- this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg(err)}`);
353783
+ this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
353599
353784
  }
353600
353785
  }
353601
353786
  // ── tls-client-error → record WHY interception failed ─────────────────────
@@ -353673,16 +353858,16 @@ async function generateCaCert(dir, onLog) {
353673
353858
  // rather than on a short expiry.
353674
353859
  });
353675
353860
  const caCertPath = (0, import_node_path.join)(dir, "validateqa-mobile-ca.pem");
353676
- await (0, import_promises.writeFile)(caCertPath, cert, "utf-8");
353861
+ await (0, import_promises2.writeFile)(caCertPath, cert, "utf-8");
353677
353862
  return { caCertPath, caKeyPem: key, caCertPem: cert };
353678
353863
  } catch (err) {
353679
- onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg(err)}`);
353864
+ onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
353680
353865
  return null;
353681
353866
  }
353682
353867
  }
353683
353868
  async function androidReadProxy(deviceId) {
353684
353869
  try {
353685
- const { stdout } = await execFileAsync(
353870
+ const { stdout } = await execFileAsync2(
353686
353871
  "adb",
353687
353872
  ["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
353688
353873
  { timeout: DEVICE_CMD_TIMEOUT_MS }
@@ -353695,33 +353880,33 @@ async function androidReadProxy(deviceId) {
353695
353880
  }
353696
353881
  async function androidSetProxy(deviceId, hostPort, onLog) {
353697
353882
  try {
353698
- await execFileAsync(
353883
+ await execFileAsync2(
353699
353884
  "adb",
353700
353885
  ["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
353701
353886
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353702
353887
  );
353703
353888
  return true;
353704
353889
  } catch (err) {
353705
- onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg(err)}`);
353890
+ onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
353706
353891
  return false;
353707
353892
  }
353708
353893
  }
353709
353894
  async function androidClearProxy(deviceId, originalProxy, onLog) {
353710
353895
  const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
353711
353896
  try {
353712
- await execFileAsync(
353897
+ await execFileAsync2(
353713
353898
  "adb",
353714
353899
  ["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
353715
353900
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353716
353901
  );
353717
353902
  } catch (err) {
353718
- onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg(err)}`);
353903
+ onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
353719
353904
  }
353720
353905
  }
353721
353906
  async function androidInstallCa(deviceId, caCertPath) {
353722
353907
  const devicePath = "/sdcard/validateqa-mobile-ca.pem";
353723
353908
  try {
353724
- await execFileAsync("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
353909
+ await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
353725
353910
  timeout: DEVICE_CMD_TIMEOUT_MS
353726
353911
  });
353727
353912
  return {
@@ -353729,23 +353914,23 @@ async function androidInstallCa(deviceId, caCertPath) {
353729
353914
  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.`
353730
353915
  };
353731
353916
  } catch (err) {
353732
- return { installed: false, note: `CA push failed: ${errMsg(err)}` };
353917
+ return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
353733
353918
  }
353734
353919
  }
353735
353920
  async function androidRemoveCa(deviceId, onLog) {
353736
353921
  try {
353737
- await execFileAsync(
353922
+ await execFileAsync2(
353738
353923
  "adb",
353739
353924
  ["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
353740
353925
  { timeout: DEVICE_CMD_TIMEOUT_MS }
353741
353926
  );
353742
353927
  } catch (err) {
353743
- onLog?.(`[mobile-net] adb remove CA failed: ${errMsg(err)}`);
353928
+ onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
353744
353929
  }
353745
353930
  }
353746
353931
  async function iosSimTrustCa(deviceId, caCertPath) {
353747
353932
  try {
353748
- await execFileAsync("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
353933
+ await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
353749
353934
  timeout: DEVICE_CMD_TIMEOUT_MS
353750
353935
  });
353751
353936
  return {
@@ -353753,18 +353938,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
353753
353938
  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."
353754
353939
  };
353755
353940
  } catch (err) {
353756
- return { trusted: false, note: `simctl add-root-cert failed: ${errMsg(err)}` };
353941
+ return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
353757
353942
  }
353758
353943
  }
353759
353944
  async function iosSimRemoveCa(deviceId, onLog) {
353760
353945
  if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
353761
353946
  try {
353762
- await execFileAsync("xcrun", ["simctl", "keychain", deviceId, "reset"], {
353947
+ await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
353763
353948
  timeout: DEVICE_CMD_TIMEOUT_MS
353764
353949
  });
353765
353950
  onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
353766
353951
  } catch (err) {
353767
- onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg(err)}`);
353952
+ onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
353768
353953
  }
353769
353954
  return;
353770
353955
  }
@@ -353772,7 +353957,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
353772
353957
  `[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.`
353773
353958
  );
353774
353959
  }
353775
- function errMsg(err) {
353960
+ function errMsg2(err) {
353776
353961
  return err instanceof Error ? err.message : String(err);
353777
353962
  }
353778
353963
  function isProcessAlive(pid) {
@@ -353863,7 +354048,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
353863
354048
  let service;
353864
354049
  let prior;
353865
354050
  try {
353866
- const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
354051
+ const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
353867
354052
  timeout: DEVICE_CMD_TIMEOUT_MS
353868
354053
  });
353869
354054
  const svc = parsePrimaryNetworkService(order);
@@ -353872,26 +354057,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
353872
354057
  }
353873
354058
  service = svc;
353874
354059
  const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
353875
- execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
353876
- execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
354060
+ execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
354061
+ execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
353877
354062
  ]);
353878
354063
  prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
353879
354064
  for (const args of buildHostProxySetCommands(service, host, port)) {
353880
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354065
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353881
354066
  }
353882
354067
  onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
353883
354068
  return { configured: true, service, prior };
353884
354069
  } catch (err) {
353885
354070
  if (service && prior) {
353886
- onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg(err)}`);
354071
+ onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
353887
354072
  for (const args of buildHostProxyRestoreCommands(service, prior)) {
353888
354073
  try {
353889
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354074
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353890
354075
  } catch {
353891
354076
  }
353892
354077
  }
353893
354078
  }
353894
- return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
354079
+ return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
353895
354080
  }
353896
354081
  }
353897
354082
  async function iosSimRestoreHostProxy(service, prior, onLog) {
@@ -353901,9 +354086,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
353901
354086
  };
353902
354087
  for (const args of buildHostProxyRestoreCommands(service, restore)) {
353903
354088
  try {
353904
- await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
354089
+ await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
353905
354090
  } catch (err) {
353906
- onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg(err)}`);
354091
+ onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
353907
354092
  }
353908
354093
  }
353909
354094
  onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
@@ -353915,14 +354100,14 @@ function markerPath(sessionKey) {
353915
354100
  }
353916
354101
  async function writePendingCleanup(sessionKey, marker) {
353917
354102
  try {
353918
- await (0, import_promises.mkdir)(PENDING_CLEANUP_DIR, { recursive: true });
353919
- await (0, import_promises.writeFile)(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
354103
+ await (0, import_promises2.mkdir)(PENDING_CLEANUP_DIR, { recursive: true });
354104
+ await (0, import_promises2.writeFile)(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
353920
354105
  } catch {
353921
354106
  }
353922
354107
  }
353923
354108
  async function removePendingCleanup(sessionKey) {
353924
354109
  try {
353925
- await (0, import_promises.unlink)(markerPath(sessionKey));
354110
+ await (0, import_promises2.unlink)(markerPath(sessionKey));
353926
354111
  } catch {
353927
354112
  }
353928
354113
  }
@@ -353955,7 +354140,7 @@ function restoreDeviceSync(marker, onLog) {
353955
354140
  async function reconcilePendingCleanups(onLog) {
353956
354141
  let files;
353957
354142
  try {
353958
- files = await (0, import_promises.readdir)(PENDING_CLEANUP_DIR);
354143
+ files = await (0, import_promises2.readdir)(PENDING_CLEANUP_DIR);
353959
354144
  } catch {
353960
354145
  return;
353961
354146
  }
@@ -353965,7 +354150,7 @@ async function reconcilePendingCleanups(onLog) {
353965
354150
  if (activeCleanups.has(sessionKey)) continue;
353966
354151
  const full = (0, import_node_path.join)(PENDING_CLEANUP_DIR, file);
353967
354152
  try {
353968
- const raw = await (0, import_promises.readFile)(full, "utf-8");
354153
+ const raw = await (0, import_promises2.readFile)(full, "utf-8");
353969
354154
  const marker = JSON.parse(raw);
353970
354155
  if (marker && typeof marker.deviceId === "string" && marker.platform) {
353971
354156
  if (!shouldReconcilePendingCleanup(marker)) {
@@ -353978,7 +354163,7 @@ async function reconcilePendingCleanups(onLog) {
353978
354163
  } catch {
353979
354164
  }
353980
354165
  try {
353981
- await (0, import_promises.unlink)(full);
354166
+ await (0, import_promises2.unlink)(full);
353982
354167
  } catch {
353983
354168
  }
353984
354169
  }
@@ -354016,7 +354201,7 @@ async function startMobileNetworkCapture(options) {
354016
354201
  let caKeyPem = null;
354017
354202
  let caCertPem = null;
354018
354203
  try {
354019
- tmpDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-ca-"));
354204
+ tmpDir = await (0, import_promises2.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-ca-"));
354020
354205
  const ca = await generateCaCert(tmpDir, onLog);
354021
354206
  if (ca) {
354022
354207
  caCertPath = ca.caCertPath;
@@ -354024,7 +354209,7 @@ async function startMobileNetworkCapture(options) {
354024
354209
  caCertPem = ca.caCertPem;
354025
354210
  }
354026
354211
  } catch (err) {
354027
- onLog?.(`[mobile-net] CA setup degraded: ${errMsg(err)}`);
354212
+ onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
354028
354213
  }
354029
354214
  if (!caKeyPem || !caCertPem) {
354030
354215
  try {
@@ -354032,9 +354217,9 @@ async function startMobileNetworkCapture(options) {
354032
354217
  caKeyPem = fallback.key;
354033
354218
  caCertPem = fallback.cert;
354034
354219
  } catch (err) {
354035
- onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg(err)}`);
354220
+ onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
354036
354221
  if (tmpDir) {
354037
- await (0, import_promises.rm)(tmpDir, { recursive: true, force: true }).catch(() => void 0);
354222
+ await (0, import_promises2.rm)(tmpDir, { recursive: true, force: true }).catch(() => void 0);
354038
354223
  }
354039
354224
  throw err instanceof Error ? err : new Error(String(err));
354040
354225
  }
@@ -354089,7 +354274,7 @@ async function startMobileNetworkCapture(options) {
354089
354274
  }
354090
354275
  }
354091
354276
  } catch (err) {
354092
- wiringReason = `device wiring degraded: ${errMsg(err)}`;
354277
+ wiringReason = `device wiring degraded: ${errMsg2(err)}`;
354093
354278
  onLog?.(`[mobile-net] ${wiringReason}`);
354094
354279
  }
354095
354280
  if (proxySet || caInstalledOnDevice || hostProxySet) {
@@ -354178,9 +354363,9 @@ async function startMobileNetworkCapture(options) {
354178
354363
  await proxy.close();
354179
354364
  if (capturedTmpDir) {
354180
354365
  try {
354181
- await (0, import_promises.rm)(capturedTmpDir, { recursive: true, force: true });
354366
+ await (0, import_promises2.rm)(capturedTmpDir, { recursive: true, force: true });
354182
354367
  } catch (err) {
354183
- onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg(err)}`);
354368
+ onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
354184
354369
  }
354185
354370
  }
354186
354371
  const apiCount = finalCalls.filter((c) => c.isApi).length;
@@ -354217,7 +354402,7 @@ var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
354217
354402
  var WD_ACTION_TIMEOUT_MS = 3e4;
354218
354403
  var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
354219
354404
  var MAX_SESSION_CREATE_ATTEMPTS = 3;
354220
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
354405
+ var execFileAsync3 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
354221
354406
  var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
354222
354407
  var WebDriverTransportError = class extends Error {
354223
354408
  isTransport = true;
@@ -354434,7 +354619,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
354434
354619
  const args = androidSystemPortForwardRemovalArgs(target, error2);
354435
354620
  if (!args) return false;
354436
354621
  try {
354437
- await execFileAsync2("adb", args, { timeout: 1e4, windowsHide: true });
354622
+ await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
354438
354623
  return true;
354439
354624
  } catch {
354440
354625
  return false;
@@ -354692,7 +354877,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
354692
354877
  if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
354693
354878
  return boundsFromStep(step);
354694
354879
  }
354695
- function locatorStrategyForValue(value) {
354880
+ function locatorStrategyForValue(value, platform3) {
354696
354881
  const trimmed = value?.trim();
354697
354882
  if (!trimmed) {
354698
354883
  return [];
@@ -354708,7 +354893,8 @@ function locatorStrategyForValue(value) {
354708
354893
  }
354709
354894
  return [
354710
354895
  { using: "accessibility id", value: trimmed },
354711
- { using: "id", value: trimmed }
354896
+ { using: "id", value: trimmed },
354897
+ ...textIdentityStrategies(trimmed, platform3)
354712
354898
  ];
354713
354899
  }
354714
354900
  function buildLocatorStrategies(target, platform3) {
@@ -354718,19 +354904,19 @@ function buildLocatorStrategies(target, platform3) {
354718
354904
  if (bundle) {
354719
354905
  if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
354720
354906
  if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
354907
+ if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354721
354908
  for (const value of [
354722
- isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
354909
+ isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
354723
354910
  bundle.contentDesc
354724
354911
  ]) {
354725
354912
  const trimmed = value?.trim();
354726
- if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
354913
+ if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
354727
354914
  }
354728
- if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
354729
354915
  }
354730
354916
  if (!(platform3 && hasPlatformLocator(target, platform3))) {
354731
- strategies.push(...locatorStrategyForValue(target.primary));
354917
+ strategies.push(...locatorStrategyForValue(target.primary, platform3));
354732
354918
  for (const fallback of target.fallbacks ?? []) {
354733
- strategies.push(...locatorStrategyForValue(fallback));
354919
+ strategies.push(...locatorStrategyForValue(fallback, platform3));
354734
354920
  }
354735
354921
  }
354736
354922
  return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
@@ -354752,12 +354938,31 @@ function identityTextXpath2(value) {
354752
354938
  const lit = xpathLiteral3(value);
354753
354939
  return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
354754
354940
  }
354941
+ function iosPredicateLiteral(value) {
354942
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
354943
+ }
354944
+ function iosTextIdentityPredicate(value) {
354945
+ const lit = iosPredicateLiteral(value);
354946
+ return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
354947
+ }
354948
+ function textIdentityStrategies(value, platform3) {
354949
+ const strategies = [];
354950
+ if (platform3 === "IOS") {
354951
+ strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
354952
+ }
354953
+ strategies.push({ using: "xpath", value: identityTextXpath2(value) });
354954
+ return strategies;
354955
+ }
354956
+ function isIdentityIosPredicate(value) {
354957
+ return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
354958
+ }
354755
354959
  function isIdentityXpath(value) {
354756
354960
  return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
354757
354961
  }
354758
354962
  function isIdentityStrategy(strategy) {
354759
354963
  if (strategy.using === "accessibility id" || strategy.using === "id") return true;
354760
354964
  if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
354965
+ if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
354761
354966
  return false;
354762
354967
  }
354763
354968
  function buildIdentityLocatorStrategies(target, platform3) {
@@ -354767,15 +354972,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
354767
354972
  if (bundle) {
354768
354973
  if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
354769
354974
  if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
354770
- for (const value of [bundle.text, bundle.contentDesc]) {
354975
+ if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354976
+ for (const value of [
354977
+ isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
354978
+ bundle.contentDesc
354979
+ ]) {
354771
354980
  const trimmed = value?.trim();
354772
- if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
354981
+ if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
354773
354982
  }
354774
- if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
354775
354983
  }
354776
354984
  if (!(platform3 && hasPlatformLocator(target, platform3))) {
354777
354985
  for (const value of [target.primary, ...target.fallbacks ?? []]) {
354778
- for (const strategy of locatorStrategyForValue(value)) {
354986
+ for (const strategy of locatorStrategyForValue(value, platform3)) {
354779
354987
  if (isIdentityStrategy(strategy)) strategies.push(strategy);
354780
354988
  }
354781
354989
  }
@@ -354885,7 +355093,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
354885
355093
  ];
354886
355094
  for (const [namespace, key, value] of settingsToDisable) {
354887
355095
  try {
354888
- await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
355096
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
354889
355097
  timeout: 8e3,
354890
355098
  windowsHide: true
354891
355099
  });
@@ -354897,14 +355105,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
354897
355105
  if (!deviceId) {
354898
355106
  throw new Error("Android keyboard fallback requires a device id");
354899
355107
  }
354900
- await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
355108
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
354901
355109
  timeout: 1e4,
354902
355110
  windowsHide: true
354903
355111
  });
354904
355112
  }
354905
355113
  async function pressAndroidKeyboardEnter(deviceId) {
354906
355114
  if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
354907
- await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
355115
+ await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
354908
355116
  timeout: 1e4,
354909
355117
  windowsHide: true
354910
355118
  });
@@ -355425,7 +355633,14 @@ async function executeMobileTest(options) {
355425
355633
  const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
355426
355634
  if (resetAllowed) {
355427
355635
  try {
355428
- await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
355636
+ if (options.target.platform === "IOS") {
355637
+ await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
355638
+ });
355639
+ const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
355640
+ if (!wiped.ok) throw new Error(wiped.detail);
355641
+ } else {
355642
+ await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
355643
+ }
355429
355644
  clearedAppDataBeforeLaunch = true;
355430
355645
  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).");
355431
355646
  } catch (err) {
@@ -355770,8 +355985,7 @@ async function readElementAttribute(sessionId, elementId, name) {
355770
355985
  function harvestFallbackXpath(platform3, attrs) {
355771
355986
  if (platform3 === "IOS") {
355772
355987
  if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
355773
- const text = attrs.label ?? attrs.value;
355774
- if (text) return identityTextXpath2(text);
355988
+ if (attrs.text) return identityTextXpath2(attrs.text);
355775
355989
  return "//*";
355776
355990
  }
355777
355991
  if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
@@ -355781,14 +355995,15 @@ function harvestFallbackXpath(platform3, attrs) {
355781
355995
  }
355782
355996
  async function readNativeElementBundle(sessionId, elementId, platform3) {
355783
355997
  const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
355998
+ const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
355784
355999
  if (platform3 === "IOS") {
355785
356000
  const name = await readElementAttribute(sessionId, elementId, "name");
355786
356001
  const label = await readElementAttribute(sessionId, elementId, "label");
355787
356002
  const value = await readElementAttribute(sessionId, elementId, "value");
355788
- const text2 = label ?? value;
356003
+ const text2 = label ?? (volatileText ? void 0 : value);
355789
356004
  if (!name && !text2) return null;
355790
356005
  const bundle2 = {
355791
- xpath: harvestFallbackXpath("IOS", { name, label, value }),
356006
+ xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
355792
356007
  className
355793
356008
  };
355794
356009
  if (name) bundle2.accessibilityId = name;
@@ -355797,7 +356012,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
355797
356012
  }
355798
356013
  const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
355799
356014
  const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
355800
- const text = await readElementAttribute(sessionId, elementId, "text");
356015
+ const rawText = await readElementAttribute(sessionId, elementId, "text");
356016
+ const text = volatileText ? void 0 : rawText;
355801
356017
  if (!resourceId && !contentDesc && !text) return null;
355802
356018
  const bundle = {
355803
356019
  xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
@@ -356082,10 +356298,10 @@ function decideMobileDeviceGate(input) {
356082
356298
 
356083
356299
  // src/services/mobile-bridge.ts
356084
356300
  var import_http = require("http");
356085
- var import_child_process3 = require("child_process");
356301
+ var import_child_process4 = require("child_process");
356086
356302
  var import_crypto = require("crypto");
356087
356303
  var import_os4 = require("os");
356088
- var import_path3 = require("path");
356304
+ var import_path4 = require("path");
356089
356305
  var import_fs3 = require("fs");
356090
356306
  init_dist();
356091
356307
  var MOBILE_BRIDGE_PORT2 = 9515;
@@ -356192,7 +356408,7 @@ async function getWindowSize3() {
356192
356408
  }
356193
356409
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
356194
356410
  var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
356195
- var BRIDGE_PID_FILE = (0, import_path3.join)((0, import_os4.homedir)(), ".validate.qa", "mobile-bridge.pid");
356411
+ var BRIDGE_PID_FILE = (0, import_path4.join)((0, import_os4.homedir)(), ".validate.qa", "mobile-bridge.pid");
356196
356412
  function readBridgePidFile() {
356197
356413
  try {
356198
356414
  const raw = (0, import_fs3.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
@@ -356204,7 +356420,7 @@ function readBridgePidFile() {
356204
356420
  }
356205
356421
  function writeBridgePidFile(pid) {
356206
356422
  try {
356207
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
356423
+ (0, import_fs3.mkdirSync)((0, import_path4.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
356208
356424
  (0, import_fs3.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
356209
356425
  } catch {
356210
356426
  }
@@ -356219,19 +356435,19 @@ function killStaleBridge() {
356219
356435
  const pid = readBridgePidFile();
356220
356436
  if (!pid) return;
356221
356437
  try {
356222
- (0, import_child_process3.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
356438
+ (0, import_child_process4.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
356223
356439
  } catch {
356224
356440
  clearBridgePidFile();
356225
356441
  return;
356226
356442
  }
356227
356443
  try {
356228
- const pidsOnPort = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
356444
+ const pidsOnPort = (0, import_child_process4.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
356229
356445
  const pidList = pidsOnPort.split("\n").filter(Boolean);
356230
356446
  if (!pidList.includes(String(pid))) {
356231
356447
  return;
356232
356448
  }
356233
- (0, import_child_process3.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
356234
- (0, import_child_process3.execFileSync)("sleep", ["1"], { stdio: "ignore" });
356449
+ (0, import_child_process4.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
356450
+ (0, import_child_process4.execFileSync)("sleep", ["1"], { stdio: "ignore" });
356235
356451
  } catch {
356236
356452
  } finally {
356237
356453
  clearBridgePidFile();
@@ -357330,6 +357546,7 @@ var MobileBridgeDriver = class {
357330
357546
  sessionId;
357331
357547
  platform;
357332
357548
  appId;
357549
+ deviceId;
357333
357550
  allowAppReset;
357334
357551
  recoverSession;
357335
357552
  maxSessionRecoveries;
@@ -357350,6 +357567,7 @@ var MobileBridgeDriver = class {
357350
357567
  this.sessionId = config.sessionId;
357351
357568
  this.platform = config.platform;
357352
357569
  this.appId = config.appId;
357570
+ this.deviceId = config.deviceId;
357353
357571
  this.allowAppReset = config.allowAppReset === true;
357354
357572
  this.recoverSession = config.recoverSession;
357355
357573
  this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
@@ -357647,17 +357865,34 @@ var MobileBridgeDriver = class {
357647
357865
  });
357648
357866
  }
357649
357867
  /**
357650
- * Deterministic state reset: clear app data + relaunch (Android emulators
357651
- * only, per the runner-computed allowAppReset policy). Returns false when
357652
- * unsupported/disallowed or when clearApp throws — callers fall back to
357653
- * relaunch(), preserving today's behavior. On success, waits for cold-boot
357654
- * content so recording and replay agree on the true entry screen.
357868
+ * Deterministic state reset: clear app data + relaunch, per the
357869
+ * runner-computed allowAppReset policy (Android emulators / iOS simulators
357870
+ * only — a personal physical device is never wiped). Android clears via
357871
+ * `mobile: clearApp`; iOS terminates the app and wipes its simulator data
357872
+ * container + keychain through `simctl` (see ios-simulator-reset.ts).
357873
+ * Returns false when unsupported/disallowed or when the clear fails —
357874
+ * callers fall back to relaunch(), preserving today's behavior. On success,
357875
+ * waits for cold-boot content so recording and replay agree on the true
357876
+ * entry screen.
357655
357877
  */
357656
357878
  async resetAppState() {
357657
- if (!this.allowAppReset || this.platform !== "ANDROID") return false;
357879
+ if (!this.allowAppReset) return false;
357880
+ if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
357658
357881
  return this.withSessionRecovery("app data reset", async () => {
357659
357882
  try {
357660
- await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
357883
+ if (this.platform === "IOS") {
357884
+ try {
357885
+ await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
357886
+ } catch (err) {
357887
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
357888
+ }
357889
+ const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
357890
+ if (!wiped.ok) {
357891
+ throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
357892
+ }
357893
+ } else {
357894
+ await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
357895
+ }
357661
357896
  await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
357662
357897
  try {
357663
357898
  await waitForColdBootSettle(this.sessionPath);
@@ -357796,7 +358031,7 @@ function readActiveBundleId2(result) {
357796
358031
  }
357797
358032
 
357798
358033
  // src/services/doctor.ts
357799
- var import_child_process4 = require("child_process");
358034
+ var import_child_process5 = require("child_process");
357800
358035
  var import_fs4 = require("fs");
357801
358036
  var import_os5 = require("os");
357802
358037
  var C = {
@@ -357816,7 +358051,7 @@ function computeDoctorReadiness(web, ios, android) {
357816
358051
  }
357817
358052
  function run(cmd) {
357818
358053
  try {
357819
- return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
358054
+ return (0, import_child_process5.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
357820
358055
  } catch {
357821
358056
  return null;
357822
358057
  }
@@ -358323,7 +358558,7 @@ var log = {
358323
358558
  dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
358324
358559
  };
358325
358560
  function getRunsDir() {
358326
- const dir = process.env.RUNS_DIR || (0, import_path4.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
358561
+ const dir = process.env.RUNS_DIR || (0, import_path5.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
358327
358562
  (0, import_runner_core12.ensurePrivateDirectory)(dir);
358328
358563
  return dir;
358329
358564
  }
@@ -360255,9 +360490,11 @@ ${finalVerifyResult.logs ?? ""}`;
360255
360490
  sessionId: session.sessionId,
360256
360491
  platform: platform3,
360257
360492
  appId: session.appId,
360258
- // Deterministic-state authorization: Android emulators only by
360259
- // default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
360260
- // physical device must never have its app data wiped.
360493
+ deviceId: session.deviceId,
360494
+ // Deterministic-state authorization: Android emulators / iOS
360495
+ // simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
360496
+ // Android default) — a personal physical device must never have
360497
+ // its app data wiped.
360261
360498
  allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
360262
360499
  maxSessionRecoveries: 1,
360263
360500
  recoverSession: async (cause) => {
@@ -360773,6 +361010,7 @@ ${finalVerifyResult.logs ?? ""}`;
360773
361010
  sessionId: opened.sessionId,
360774
361011
  platform: target.platform,
360775
361012
  appId: opened.appId,
361013
+ deviceId: opened.deviceId,
360776
361014
  allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
360777
361015
  });
360778
361016
  };