@validate.qa/runner 1.0.31 → 1.0.33

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 +645 -172
  2. package/dist/cli.mjs +645 -172
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5001,6 +5001,29 @@ var require_mobile_observation = __commonJS({
5001
5001
  }
5002
5002
  });
5003
5003
 
5004
+ // ../runner-core/dist/services/mobile/mobile-keyboard.js
5005
+ var require_mobile_keyboard = __commonJS({
5006
+ "../runner-core/dist/services/mobile/mobile-keyboard.js"(exports2) {
5007
+ "use strict";
5008
+ Object.defineProperty(exports2, "__esModule", { value: true });
5009
+ exports2.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
5010
+ exports2.dismissKeyboardForRetry = dismissKeyboardForRetry;
5011
+ var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
5012
+ var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
5013
+ function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
5014
+ if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
5015
+ return false;
5016
+ return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
5017
+ }
5018
+ async function dismissKeyboardForRetry(driver) {
5019
+ if (typeof driver.dismissKeyboard === "function") {
5020
+ return driver.dismissKeyboard();
5021
+ }
5022
+ return driver.back();
5023
+ }
5024
+ }
5025
+ });
5026
+
5004
5027
  // ../runner-core/dist/utils/scrub-credentials.js
5005
5028
  var require_scrub_credentials = __commonJS({
5006
5029
  "../runner-core/dist/utils/scrub-credentials.js"(exports2) {
@@ -61982,6 +62005,7 @@ var require_mobile_explorer = __commonJS({
61982
62005
  var mobile_source_parser_js_1 = require_mobile_source_parser();
61983
62006
  var mobile_observation_js_1 = require_mobile_observation();
61984
62007
  var mobile_boundary_js_1 = require_mobile_boundary();
62008
+ var mobile_keyboard_js_1 = require_mobile_keyboard();
61985
62009
  var credential_tools_js_1 = require_credential_tools();
61986
62010
  var verification_inbox_js_1 = require_verification_inbox();
61987
62011
  var inbox_tools_js_1 = require_inbox_tools();
@@ -61991,6 +62015,7 @@ var require_mobile_explorer = __commonJS({
61991
62015
  var MAX_RECENT_EXCHANGES = 24;
61992
62016
  var OBSERVATION_MAX_ELEMENTS = 50;
61993
62017
  var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
62018
+ var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
61994
62019
  var MOBILE_SNAPSHOT_TOOL = {
61995
62020
  type: "function",
61996
62021
  function: {
@@ -62260,7 +62285,7 @@ var require_mobile_explorer = __commonJS({
62260
62285
  return { screen, signal, refMap, observation };
62261
62286
  }
62262
62287
  function buildStatePacket(input) {
62263
- const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
62288
+ const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
62264
62289
  const sections = [];
62265
62290
  sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
62266
62291
  sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
@@ -62273,10 +62298,46 @@ var require_mobile_explorer = __commonJS({
62273
62298
  sections.push("");
62274
62299
  if (mode === "survey") {
62275
62300
  sections.push("**Mission:** Map every reachable screen. For each new screen: mobile_snapshot -> capture_screen -> record_transition. Get PAST the entry gate into the app's real main content \u2014 the login/landing screens are the ENTRANCE, not the app. If test credentials are provided (see the Test Credentials section), SIGN IN with them FIRST and map the authenticated app; that is the real product surface. Prefer a real login when credentials are provided; only if login genuinely cannot be completed after honest attempts (persistent error, the form will not accept the credential, no valid account) fall back to any guest/demo/skip entry the app offers and map the app fully from there \u2014 reaching the real feature surface that way is far better coverage than getting stuck on the login wall. Do NOT submit forms with junk data or mutate real data. Call finish_exploration ONLY when every reachable screen \u2014 including the authenticated content \u2014 has been captured and no unvisited frontier screens remain.");
62301
+ } else if (featureName?.trim()) {
62302
+ sections.push(`**Mission:** Feature-scoped deep exploration of **"${featureName.trim()}"**. Drive the app like a real user through THIS feature only: tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Do NOT deep-dive unrelated features. Call finish_exploration when this feature's flows are covered.`);
62276
62303
  } else {
62277
62304
  sections.push("**Mission:** Use the focus features like a real user. Tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Call finish_exploration when the flows are covered.");
62278
62305
  }
62279
62306
  sections.push("");
62307
+ if (mode === "deep" && featureName?.trim()) {
62308
+ sections.push(`## Feature: ${featureName.trim()}`);
62309
+ if (featureContext?.plannerDescription?.trim()) {
62310
+ sections.push(featureContext.plannerDescription.trim());
62311
+ }
62312
+ if (featureContext?.priorInsight?.trim()) {
62313
+ sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
62314
+ }
62315
+ if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
62316
+ sections.push("### Target screens / routes for this feature");
62317
+ for (const page of featureContext.pageIntel.slice(0, 12)) {
62318
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
62319
+ }
62320
+ }
62321
+ if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
62322
+ sections.push("### Known flows");
62323
+ for (const flow of featureContext.flowIntel.slice(0, 8)) {
62324
+ sections.push(`- ${flow}`);
62325
+ }
62326
+ }
62327
+ if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
62328
+ sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
62329
+ for (const other of featureContext.otherFeatures.slice(0, 12)) {
62330
+ const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
62331
+ sections.push(`- ${other.name}${pages}`);
62332
+ }
62333
+ }
62334
+ sections.push("");
62335
+ }
62336
+ if (userNotes?.trim()) {
62337
+ sections.push(`## Operator notes
62338
+ ${userNotes.trim()}`);
62339
+ sections.push("");
62340
+ }
62280
62341
  const screenCount = state2.getScreenCount();
62281
62342
  sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
62282
62343
  const currentScreenId = state2.getCurrentScreenId();
@@ -62433,7 +62494,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62433
62494
  } catch {
62434
62495
  }
62435
62496
  };
62436
- const mode = ctx.mode === "deep" ? "deep" : "survey";
62497
+ const mode = ctx.mode === "survey" ? "survey" : "deep";
62437
62498
  const platform3 = platformForMedium(ctx.medium);
62438
62499
  const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
62439
62500
  const targetAppId = ctx.target?.appId?.trim() || void 0;
@@ -62556,9 +62617,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62556
62617
  externalOwner: boundary.owner
62557
62618
  };
62558
62619
  }
62559
- log2(` Relaunch still outside target app (${recoveredBoundary.owner}); no screen recorded.`);
62620
+ let lastExternalOwner = recoveredBoundary.owner;
62621
+ for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
62622
+ try {
62623
+ await driver.launchApp();
62624
+ const retrySnap = await driver.snapshot();
62625
+ const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
62626
+ if (!retryRecovered)
62627
+ continue;
62628
+ const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
62629
+ latest = retryRecovered;
62630
+ lastSnapshotIteration = iterationForObservation;
62631
+ if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
62632
+ const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
62633
+ captureScreenshot(retrySnap.screenshot, screenId);
62634
+ log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
62635
+ return {
62636
+ observation: retryRecovered.observation,
62637
+ screenId,
62638
+ externalBlocked: true,
62639
+ externalOwner: boundary.owner
62640
+ };
62641
+ }
62642
+ if (retryBoundary.kind === "transient_external") {
62643
+ log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
62644
+ return {
62645
+ observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
62646
+ screenId: null,
62647
+ externalBlocked: true,
62648
+ transientExternal: true,
62649
+ externalOwner: boundary.owner
62650
+ };
62651
+ }
62652
+ lastExternalOwner = retryBoundary.owner;
62653
+ } catch (err) {
62654
+ log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
62655
+ }
62656
+ }
62657
+ log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
62560
62658
  return {
62561
- observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no screen was recorded)`,
62659
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
62562
62660
  screenId: null,
62563
62661
  externalBlocked: true,
62564
62662
  externalOwner: boundary.owner
@@ -62684,7 +62782,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62684
62782
  maxIterations,
62685
62783
  latestObservation: getLatest()?.observation ?? null,
62686
62784
  journeys,
62687
- transitions: collectedTransitions
62785
+ transitions: collectedTransitions,
62786
+ featureName: ctx.featureName,
62787
+ featureContext: ctx.featureContext,
62788
+ userNotes: ctx.userNotes
62688
62789
  });
62689
62790
  const messages = [
62690
62791
  { role: "system", content: systemPrompt },
@@ -62936,7 +63037,19 @@ Exploration complete: ${summary}`);
62936
63037
  const fromScreenId = state2.getCurrentScreenId();
62937
63038
  const boundaryBeforeAction = currentBoundary();
62938
63039
  const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
62939
- const actionResult = await driver.tap(resolved.bounds);
63040
+ let actionResult = await driver.tap(resolved.bounds);
63041
+ if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
63042
+ log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
63043
+ const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
63044
+ const retryResult = await driver.tap(resolved.bounds);
63045
+ actionResult = retryResult.ok ? retryResult : {
63046
+ ...retryResult,
63047
+ error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
63048
+ source: retryResult.source ?? dismissResult.source,
63049
+ screenshot: retryResult.screenshot ?? dismissResult.screenshot,
63050
+ screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
63051
+ };
63052
+ }
62940
63053
  const absorbed = await absorbActionResult(actionResult, "mobile_tap");
62941
63054
  const { observation, screenId } = absorbed;
62942
63055
  if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
@@ -65936,6 +66049,25 @@ var require_mobile_discovery_prompts = __commonJS({
65936
66049
  function targetAppScreens(graph, appId) {
65937
66050
  return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
65938
66051
  }
66052
+ function isGenericNonFeatureScreen(state2) {
66053
+ const type = state2.screenType?.trim().toLowerCase();
66054
+ if (type === "error" || type === "permission")
66055
+ return true;
66056
+ if (type && type !== "unknown")
66057
+ return false;
66058
+ const actionableCount = state2.elements.filter((el) => el.actionable).length;
66059
+ if (actionableCount > 0)
66060
+ return false;
66061
+ const evidenceText = [
66062
+ state2.screenName,
66063
+ state2.screenType,
66064
+ ...state2.observations ?? []
66065
+ ].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
66066
+ return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
66067
+ }
66068
+ function plannableTargetAppScreens(graph, appId) {
66069
+ return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
66070
+ }
65939
66071
  exports2.MOBILE_SURVEY_SYSTEM_PROMPT = `You are a feature analyst for a native mobile application (iOS or Android). You receive evidence from an AI explorer that mapped the app's screens by tapping, typing, and swiping through the live UI. Your job is to organize screens into meaningful feature groups.
65940
66072
 
65941
66073
  ## Output Discipline
@@ -65955,8 +66087,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
65955
66087
  5. CRUD screens for the same entity belong together
65956
66088
  6. Do NOT merge screens into one feature just because they serve a similar broader goal. Treat areas as separate features when the evidence shows separate primary navigation destinations (e.g. separate tab-bar items or drawer links), separate screen families for distinct entities, or different CRUD entities/forms. Keep screens together when they share one navigational area and operate on the same entity (e.g. a list screen, its filter/sort sheet, and its detail screen all belong together).
65957
66089
  7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
65958
- 8. Do NOT create standalone features for generic error / empty screens (network-error, permission-denied). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description.
65959
- 9. COVER EVERY SCREEN: every explored screen except generic error / permission screens MUST appear in exactly one feature group's routes. Do not leave screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous screen to its most closely related feature and note the ambiguity in that feature's description. Sign-in / sign-up / forgot-password screens form an "Authentication" group even when they were only reached via links from other screens.
66090
+ 8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature.
66091
+ 9. COVER EVERY PRODUCT SCREEN: every explored product screen except generic error / permission / empty / loading / intermediate screens MUST appear in exactly one feature group's routes. Do not leave usable screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous usable screen to its most closely related feature and note the ambiguity in that feature's description. Sign-in / sign-up / forgot-password screens form an "Authentication" group even when they were only reached via links from other screens.
65960
66092
  10. LOGGED-OUT vs LOGGED-IN SEPARATION: screens shown BEFORE the user is signed in (splash, landing/welcome, theme or language pickers, walkthroughs, sign-up, sign-in, forgot-password, demo/preview screens) must NEVER share a feature group with the signed-in app. Put sign-in / sign-up / forgot-password in "Authentication" (rule 4) and the remaining pre-login screens in an "Onboarding" group. Features named after signed-in areas (feeds, dashboards, tabs, settings) must contain ONLY screens reached after authentication. A pre-login flow is a linear sequence, not a navigation destination \u2014 do not let it swallow the app's main features.
65961
66093
 
65962
66094
  ## Output JSON Schema
@@ -66222,8 +66354,36 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
66222
66354
  if (context.featureName) {
66223
66355
  sections.push(`## Feature: ${context.featureName}
66224
66356
  SCOPE: this is a feature-focused deep run. Plan scenarios ONLY for the "${context.featureName}" feature \u2014 its screens, forms, and flows. Screens belonging to other features are navigation context / prerequisites only; do NOT plan scenarios whose primary goal exercises another feature.`);
66357
+ if (context.featureContext?.plannerDescription?.trim()) {
66358
+ sections.push(context.featureContext.plannerDescription.trim());
66359
+ }
66360
+ if (context.featureContext?.priorInsight?.trim()) {
66361
+ sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
66362
+ }
66363
+ if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
66364
+ sections.push("### Feature page intel");
66365
+ for (const page of context.featureContext.pageIntel.slice(0, 12)) {
66366
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
66367
+ }
66368
+ }
66369
+ if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
66370
+ sections.push("### Known flows");
66371
+ for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
66372
+ sections.push(`- ${flow}`);
66373
+ }
66374
+ }
66375
+ if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
66376
+ sections.push("### Other features (do not plan primary scenarios for these)");
66377
+ for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
66378
+ sections.push(`- ${other.name}`);
66379
+ }
66380
+ }
66381
+ }
66382
+ if (context.userNotes?.trim()) {
66383
+ sections.push(`## Operator notes
66384
+ ${context.userNotes.trim()}`);
66225
66385
  }
66226
- const orderedScreens = targetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66386
+ const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66227
66387
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
66228
66388
  sections.push(`## Screens Discovered (${orderedScreens.length})`);
66229
66389
  for (const state2 of orderedScreens) {
@@ -66332,7 +66492,7 @@ ${constraints.join("\n")}`);
66332
66492
  const now = /* @__PURE__ */ new Date();
66333
66493
  const idByScreen = /* @__PURE__ */ new Map();
66334
66494
  let pageSeq = 0;
66335
- const orderedScreens = targetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66495
+ const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66336
66496
  for (const state2 of orderedScreens) {
66337
66497
  idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
66338
66498
  }
@@ -66348,7 +66508,7 @@ ${constraints.join("\n")}`);
66348
66508
  routePattern: null,
66349
66509
  title: state2.screenName ?? null,
66350
66510
  pageType: state2.screenType ?? "UNKNOWN",
66351
- requiresAuth: false,
66511
+ requiresAuth: state2.screenType?.toLowerCase() === "auth",
66352
66512
  visitCount: state2.visited ? 1 : 0,
66353
66513
  lastVisitedAt: state2.visited ? now : null,
66354
66514
  elementsCovered: 0,
@@ -66368,6 +66528,8 @@ ${constraints.join("\n")}`);
66368
66528
  const toPageId = idByScreen.get(t.toScreenId);
66369
66529
  if (!fromPageId || !toPageId)
66370
66530
  continue;
66531
+ const fromScreen = graph.screens.get(t.fromScreenId);
66532
+ const toScreen = graph.screens.get(t.toScreenId);
66371
66533
  transitions.push({
66372
66534
  id: `mtx-${txSeq++}`,
66373
66535
  fromPageId,
@@ -66375,7 +66537,7 @@ ${constraints.join("\n")}`);
66375
66537
  triggerType: t.action.toUpperCase(),
66376
66538
  triggerLabel: t.viaRef ?? null,
66377
66539
  observedCount: 1,
66378
- requiresAuth: false
66540
+ requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
66379
66541
  });
66380
66542
  }
66381
66543
  return {
@@ -66393,7 +66555,7 @@ ${constraints.join("\n")}`);
66393
66555
  }
66394
66556
  function buildMobileFeatureGroupsFromGraph(graph, appId) {
66395
66557
  const groups = /* @__PURE__ */ new Map();
66396
- const orderedScreens = targetAppScreens(graph, appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66558
+ const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66397
66559
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
66398
66560
  for (const state2 of orderedScreens) {
66399
66561
  const screenTypeLower = state2.screenType?.toLowerCase();
@@ -66440,7 +66602,7 @@ ${constraints.join("\n")}`);
66440
66602
  return [];
66441
66603
  if (input.featureName?.trim())
66442
66604
  return [];
66443
- const orderedScreens = targetAppScreens(input.graph, input.appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66605
+ const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66444
66606
  if (orderedScreens.length === 0)
66445
66607
  return [];
66446
66608
  const entryScreenId = orderedScreens[0].screenId;
@@ -66578,7 +66740,9 @@ ${constraints.join("\n")}`);
66578
66740
  recordedJourneys,
66579
66741
  credentials: input.credentials,
66580
66742
  testBudget: input.testBudget,
66581
- incremental: input.incremental
66743
+ incremental: input.incremental,
66744
+ userNotes: input.userNotes,
66745
+ featureContext: input.featureContext
66582
66746
  })
66583
66747
  });
66584
66748
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
@@ -66642,9 +66806,11 @@ var require_mobile_generator = __commonJS({
66642
66806
  var mobile_source_parser_js_2 = require_mobile_source_parser();
66643
66807
  var mobile_observation_js_1 = require_mobile_observation();
66644
66808
  var mobile_boundary_js_1 = require_mobile_boundary();
66809
+ var mobile_keyboard_js_1 = require_mobile_keyboard();
66645
66810
  var MAX_SCENARIO_ITERATIONS = 30;
66646
66811
  var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
66647
66812
  var MAX_RECENT_EXCHANGES = 24;
66813
+ var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
66648
66814
  var OBSERVATION_MAX_ELEMENTS = 50;
66649
66815
  function finalizeAction(input) {
66650
66816
  return { ...input, url: "", selector: input.selector ?? "" };
@@ -66835,8 +67001,38 @@ var require_mobile_generator = __commonJS({
66835
67001
  return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
66836
67002
  }
66837
67003
  var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
67004
+ var EDITABLE_IDENTITY_RE = /\b(email|e-?mail|password|passwd|passcode|username|user[_-]?name|login|search|query|otp|pin|phone|mobile|input|textfield|text[_-]?field|edit|typehere)\b/i;
66838
67005
  function isEditableMobileElement(element) {
66839
- return EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim());
67006
+ if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
67007
+ return true;
67008
+ const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67009
+ if (identity && EDITABLE_IDENTITY_RE.test(identity))
67010
+ return true;
67011
+ return false;
67012
+ }
67013
+ function isIdentityBearingTapTarget(element) {
67014
+ if (weakAssertionTargetReason(element))
67015
+ return false;
67016
+ if (isBareContainerLikeElement(element))
67017
+ return false;
67018
+ if (isEditableMobileElement(element))
67019
+ return false;
67020
+ if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
67021
+ return true;
67022
+ const label = [
67023
+ element.label,
67024
+ element.text,
67025
+ element.accessibilityId,
67026
+ element.resourceId
67027
+ ].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67028
+ return /\b(sign\s*in|log\s*in|login|submit|save|continue|create|send|confirm|done|next|apply|checkout|pay|finish|update|register|sign\s*up)\b/i.test(label);
67029
+ }
67030
+ function isBareContainerLikeElement(element) {
67031
+ if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
67032
+ const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
67033
+ return !hasIdentity;
67034
+ }
67035
+ return false;
66840
67036
  }
66841
67037
  function nonEmpty2(value) {
66842
67038
  if (typeof value !== "string")
@@ -67060,10 +67256,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
67060
67256
  externalOwner: boundary.owner
67061
67257
  };
67062
67258
  }
67063
- log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
67259
+ let lastExternalOwner = recoveredBoundary.owner;
67260
+ let lastRecovered = recovered;
67261
+ for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
67262
+ try {
67263
+ await driver.launchApp();
67264
+ const retrySnap = await driver.snapshot();
67265
+ const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
67266
+ if (!retryRecovered)
67267
+ continue;
67268
+ lastRecovered = retryRecovered;
67269
+ const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
67270
+ setLatest(retryRecovered);
67271
+ if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
67272
+ emitScreenshot(retrySnap.screenshot);
67273
+ log2(` recovered target app after ${attempt} additional activation attempt(s).`);
67274
+ return {
67275
+ resolved: retryRecovered,
67276
+ observation: retryRecovered.observation,
67277
+ externalBlocked: true,
67278
+ externalOwner: boundary.owner
67279
+ };
67280
+ }
67281
+ if (retryBoundary.kind === "transient_external") {
67282
+ log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
67283
+ return {
67284
+ resolved: retryRecovered,
67285
+ observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
67286
+ externalBlocked: true,
67287
+ transientExternal: true,
67288
+ externalOwner: boundary.owner
67289
+ };
67290
+ }
67291
+ lastExternalOwner = retryBoundary.owner;
67292
+ } catch (err) {
67293
+ log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
67294
+ }
67295
+ }
67296
+ log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
67064
67297
  return {
67065
- resolved: recovered,
67066
- observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
67298
+ resolved: lastRecovered,
67299
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
67067
67300
  externalBlocked: true,
67068
67301
  externalOwner: boundary.owner
67069
67302
  };
@@ -67277,9 +67510,26 @@ ${statePacket}` },
67277
67510
  return unknownRef(toolArgs.ref);
67278
67511
  const beforeScreenName = screenName();
67279
67512
  const before = latest();
67280
- const result = await driver.tap(element.bounds);
67513
+ let result = await driver.tap(element.bounds);
67514
+ if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
67515
+ log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
67516
+ const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
67517
+ const retryResult = await driver.tap(element.bounds);
67518
+ result = retryResult.ok ? retryResult : {
67519
+ ...retryResult,
67520
+ error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
67521
+ source: retryResult.source ?? dismissResult.source,
67522
+ screenshot: retryResult.screenshot ?? dismissResult.screenshot,
67523
+ screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
67524
+ };
67525
+ }
67281
67526
  const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
67282
- if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
67527
+ const screenChanged = changedScreen(before, absorbed);
67528
+ const identityTap = isIdentityBearingTapTarget(element);
67529
+ if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
67530
+ if (!screenChanged && identityTap) {
67531
+ log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
67532
+ }
67283
67533
  recordAction({
67284
67534
  type: "TAP",
67285
67535
  selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
@@ -67423,6 +67673,13 @@ ${statePacket}` },
67423
67673
  const element = resolveRef(toolArgs.ref);
67424
67674
  if (!element)
67425
67675
  return unknownRef(toolArgs.ref);
67676
+ const weakReason = weakAssertionTargetReason(element);
67677
+ if (weakReason) {
67678
+ return {
67679
+ ok: false,
67680
+ error: `Ref ${element.ref} is not a durable assertion target (${weakReason}). Re-snapshot and assert a human-visible label, text, or stable control that proves the screen outcome.`
67681
+ };
67682
+ }
67426
67683
  const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
67427
67684
  if (!expected)
67428
67685
  return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
@@ -67681,19 +67938,46 @@ ${statePacket}` },
67681
67938
  if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
67682
67939
  return "auth/session scenario tapped a login or sign-in control without recording a credential path";
67683
67940
  }
67684
- if (!isCredentialSignIn)
67941
+ if (isCredentialSignIn) {
67942
+ const typedEmail = typedEmailIndexes.length > 0;
67943
+ const typedPassword = typedPasswordIndexes.length > 0;
67944
+ const tappedSubmit = submitAfterCredentials;
67945
+ if (typedEmail && typedPassword && tappedSubmit) {
67946
+ } else {
67947
+ const missing = [
67948
+ typedEmail ? null : "email/username entry",
67949
+ typedPassword ? null : "password entry",
67950
+ tappedSubmit ? null : "sign-in submit tap"
67951
+ ].filter((value) => value !== null);
67952
+ return `credential sign-in scenario did not record ${missing.join(", ")}`;
67953
+ }
67954
+ }
67955
+ const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
67956
+ if (!isHappyPath)
67685
67957
  return null;
67686
- const typedEmail = typedEmailIndexes.length > 0;
67687
- const typedPassword = typedPasswordIndexes.length > 0;
67688
- const tappedSubmit = submitAfterCredentials;
67689
- if (typedEmail && typedPassword && tappedSubmit)
67958
+ let lastTypeIndex = -1;
67959
+ let lastAssertIndex = -1;
67960
+ actions.forEach((action, index) => {
67961
+ if (action.type === "TYPE")
67962
+ lastTypeIndex = index;
67963
+ if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
67964
+ lastAssertIndex = index;
67965
+ });
67966
+ if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
67690
67967
  return null;
67691
- const missing = [
67692
- typedEmail ? null : "email/username entry",
67693
- typedPassword ? null : "password entry",
67694
- tappedSubmit ? null : "sign-in submit tap"
67695
- ].filter((value) => value !== null);
67696
- return `credential sign-in scenario did not record ${missing.join(", ")}`;
67968
+ const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
67969
+ const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
67970
+ const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
67971
+ const commitAfterType = actions.some((action, index) => {
67972
+ if (action.type !== "TAP" || index <= lastTypeIndex || index >= lastAssertIndex)
67973
+ return false;
67974
+ const textForAction = actionText(action);
67975
+ return /\b(sign\s*in|log\s*in|login|continue|submit|save|create|send|confirm|done|next|apply|post|checkout|pay|finish|update|register|sign\s*up)\b/i.test(textForAction);
67976
+ });
67977
+ if (screensDiffer && !commitAfterType) {
67978
+ return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
67979
+ }
67980
+ return null;
67697
67981
  }
67698
67982
  function actionCycleKey(action) {
67699
67983
  return `${action.type}|${action.selector}|${action.value ?? ""}`;
@@ -68628,8 +68912,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
68628
68912
  function coerceSteps(value) {
68629
68913
  if (!Array.isArray(value) || value.length === 0)
68630
68914
  return void 0;
68631
- const allValid = value.every((step) => isRecord(step) && typeof step.action === "string" && typeof step.order === "number");
68632
- return allValid ? value : void 0;
68915
+ const steps = [];
68916
+ for (let i = 0; i < value.length; i++) {
68917
+ const step = value[i];
68918
+ if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
68919
+ return void 0;
68920
+ }
68921
+ const orderRaw = step.order;
68922
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
68923
+ steps.push({
68924
+ ...step,
68925
+ order,
68926
+ action: step.action
68927
+ });
68928
+ }
68929
+ return steps.map((step, order) => ({ ...step, order }));
68633
68930
  }
68634
68931
  function parseHealVerdict(content) {
68635
68932
  const cleaned = stripCodeFences(content);
@@ -72850,6 +73147,48 @@ var require_snapshot_observation = __commonJS({
72850
73147
  var PROMOTED_FORM_FIELD_CAP = 12;
72851
73148
  var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
72852
73149
  var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
73150
+ var DEEP_NONNAV_CAP_CENSUS = 16;
73151
+ var CENSUS_NAV_LINK_CAP = 60;
73152
+ var CENSUS_MIN_ACTION_GROUP = 3;
73153
+ var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
73154
+ function leadingActionVerb(label) {
73155
+ if (!label)
73156
+ return null;
73157
+ const m = label.trim().match(/[A-Za-z]+/);
73158
+ return m ? m[0].toLowerCase() : null;
73159
+ }
73160
+ function isNavLinkEntry(entry) {
73161
+ return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
73162
+ }
73163
+ function isRowRepeatedControl(element) {
73164
+ return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
73165
+ }
73166
+ function rowContainerScope(containerPath) {
73167
+ if (!containerPath)
73168
+ return null;
73169
+ const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
73170
+ return m ? m[1].trim() : containerPath.trim();
73171
+ }
73172
+ function containerScopeLabel(scope) {
73173
+ if (!scope)
73174
+ return null;
73175
+ const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
73176
+ if (landmarks.length > 0)
73177
+ return landmarks[landmarks.length - 1][1].trim();
73178
+ const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
73179
+ return segs.length ? segs[segs.length - 1] : null;
73180
+ }
73181
+ function rowIdentityKey(element) {
73182
+ const rc = element.enrichment?.rowContext;
73183
+ if (rc)
73184
+ return `rc:${rc}`;
73185
+ const cp = element.containerPath;
73186
+ if (cp) {
73187
+ const m = cp.match(/>\s*row:(.+)$/i);
73188
+ return m ? `row:${m[1].trim()}` : `cp:${cp}`;
73189
+ }
73190
+ return element.ref ? `ref:${element.ref}` : "unknown";
73191
+ }
72853
73192
  function isGroupedFormField(element) {
72854
73193
  if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
72855
73194
  return false;
@@ -72865,6 +73204,7 @@ var require_snapshot_observation = __commonJS({
72865
73204
  const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
72866
73205
  const artifact = page.snapshotArtifact;
72867
73206
  const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
73207
+ const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
72868
73208
  const activeScope = inferActiveSnapshotScope(page);
72869
73209
  const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
72870
73210
  const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
@@ -72878,10 +73218,17 @@ var require_snapshot_observation = __commonJS({
72878
73218
  });
72879
73219
  let interactive;
72880
73220
  if (options.mode === "survey") {
72881
- const navLinks = allInteractive.filter(({ element }) => element.elementType === "LINK" && element.triggersNavigation);
72882
- const others = allInteractive.filter(({ element }) => !(element.elementType === "LINK" && element.triggersNavigation));
73221
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73222
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
72883
73223
  const othersCap = richRender ? 6 : 4;
72884
73224
  interactive = [...navLinks, ...others.slice(0, othersCap)];
73225
+ } else if (rowActionCensus) {
73226
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73227
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
73228
+ interactive = [
73229
+ ...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
73230
+ ...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
73231
+ ];
72885
73232
  } else {
72886
73233
  interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
72887
73234
  }
@@ -72894,6 +73241,36 @@ var require_snapshot_observation = __commonJS({
72894
73241
  }
72895
73242
  }
72896
73243
  }
73244
+ if (rowActionCensus && interactive.length > 0) {
73245
+ const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
73246
+ const bestByVerb = /* @__PURE__ */ new Map();
73247
+ for (const entry of allInteractive) {
73248
+ if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
73249
+ continue;
73250
+ const verb = leadingActionVerb(qualifyElementLabel(entry.element));
73251
+ if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
73252
+ continue;
73253
+ if (shownVerbs.has(verb))
73254
+ continue;
73255
+ if (interactive.includes(entry))
73256
+ continue;
73257
+ const existing = bestByVerb.get(verb);
73258
+ if (!existing) {
73259
+ bestByVerb.set(verb, entry);
73260
+ continue;
73261
+ }
73262
+ if (existing.element.isDisabled && !entry.element.isDisabled)
73263
+ bestByVerb.set(verb, entry);
73264
+ }
73265
+ const swapIns = [...bestByVerb.values()];
73266
+ if (swapIns.length > 0) {
73267
+ const navShown = interactive.filter(isNavLinkEntry);
73268
+ const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
73269
+ const evictable = Math.min(swapIns.length, nonNavShown.length);
73270
+ const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
73271
+ interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
73272
+ }
73273
+ }
72897
73274
  const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
72898
73275
  const allForms = artifact?.formGroups ?? page.formGroups ?? [];
72899
73276
  const forms = allForms.slice(0, options.formLimit ?? 3);
@@ -73193,6 +73570,54 @@ var require_snapshot_observation = __commonJS({
73193
73570
  const hidden = allInteractive.length - interactive.length;
73194
73571
  if (hidden > 0) {
73195
73572
  lines.push(` # +${hidden} more interactive element(s) not individually listed (capped) \u2014 some may appear in the duplicates block above; if your target isn't here, narrow the scope or act on it by ref.`);
73573
+ if (rowActionCensus) {
73574
+ const shownSet = new Set(interactive.map((entry) => entry.element));
73575
+ const groups = /* @__PURE__ */ new Map();
73576
+ for (const entry of allInteractive) {
73577
+ const el = entry.element;
73578
+ if (!isRowRepeatedControl(el))
73579
+ continue;
73580
+ const verb = leadingActionVerb(qualifyElementLabel(el));
73581
+ if (!verb)
73582
+ continue;
73583
+ const role = (el.role ?? el.elementType ?? "control").toLowerCase();
73584
+ const scope = rowContainerScope(el.containerPath);
73585
+ const key = JSON.stringify([scope ?? "", role, verb]);
73586
+ let group = groups.get(key);
73587
+ if (!group) {
73588
+ group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
73589
+ groups.set(key, group);
73590
+ }
73591
+ const rowKey = rowIdentityKey(el);
73592
+ group.rows.add(rowKey);
73593
+ group.controls += 1;
73594
+ if (shownSet.has(el))
73595
+ group.shownRows.add(rowKey);
73596
+ if (el.isDisabled)
73597
+ group.disabledRows.add(rowKey);
73598
+ }
73599
+ const collapsed = [...groups.values()].map((g) => ({
73600
+ verb: g.verb,
73601
+ role: g.role,
73602
+ scopeLabel: g.scopeLabel,
73603
+ total: g.rows.size,
73604
+ shown: g.shownRows.size,
73605
+ disabled: g.disabledRows.size,
73606
+ controls: g.controls
73607
+ })).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
73608
+ const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
73609
+ const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
73610
+ return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
73611
+ });
73612
+ for (const g of collapsed) {
73613
+ const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
73614
+ const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
73615
+ const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
73616
+ const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
73617
+ const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
73618
+ lines.push(` # \u21B3 "${verbLabel}" ${g.role}${destructiveTag}${where}: present on ${g.total} rows${controlsNote} \u2014 ${g.shown} listed above, ${g.total - g.shown} collapsed${disabledNote}; the same action repeats down these rows \u2014 target a specific row by its row identity (the row's Edit link / row_context above), not a bare "${verbLabel}" locator.`);
73619
+ }
73620
+ }
73196
73621
  }
73197
73622
  }
73198
73623
  }
@@ -74215,13 +74640,14 @@ var require_auth_recipe = __commonJS({
74215
74640
  "../runner-core/dist/services/auth-recipe.js"(exports2) {
74216
74641
  "use strict";
74217
74642
  Object.defineProperty(exports2, "__esModule", { value: true });
74218
- exports2.AUTH_MARKER_VERSION = void 0;
74643
+ exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
74219
74644
  exports2.computeRecipeHash = computeRecipeHash;
74220
74645
  exports2.buildAuthRecipe = buildAuthRecipe;
74221
74646
  exports2.authProofLocator = authProofLocator;
74222
74647
  exports2.urlProofPredicateExpr = urlProofPredicateExpr;
74223
74648
  exports2.renderAuthProofAssertion = renderAuthProofAssertion;
74224
74649
  exports2.renderAuthFn = renderAuthFn;
74650
+ exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
74225
74651
  exports2.absoluteRoute = absoluteRoute;
74226
74652
  exports2.renderReplayClickArrow = renderReplayClickArrow;
74227
74653
  exports2.renderReplaySettleArrow = renderReplaySettleArrow;
@@ -74340,6 +74766,17 @@ var require_auth_recipe = __commonJS({
74340
74766
  lines.push(END_MARKER);
74341
74767
  return lines.join("\n");
74342
74768
  }
74769
+ exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
74770
+ "AUTH_FILE",
74771
+ "SHARED_AUTH_INJECT",
74772
+ "VALIDATE_RUN_ID",
74773
+ "VALIDATE_TASK_ARN",
74774
+ "VALIDATE_RUNNER_AUTH",
74775
+ "VALIDATE_SERVER_URL"
74776
+ ]);
74777
+ function isReservedRunnerEnvVar(key) {
74778
+ return key.startsWith("VALIDATE_") || exports2.RESERVED_RUNNER_ENV_VARS.has(key);
74779
+ }
74343
74780
  function absoluteRoute(route2, baseUrl) {
74344
74781
  try {
74345
74782
  return new URL(route2, baseUrl).href;
@@ -315412,8 +315849,8 @@ var require_mobile_discovery_agent = __commonJS({
315412
315849
  };
315413
315850
  }
315414
315851
  log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
315415
- const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
315416
- log2(exploreAppDataCleared ? "App data cleared \u2014 exploring from a deterministic cold (logged-out) state." : target?.resetPolicy === "PRESERVE_APP_DATA" ? "App data reset skipped by policy \u2014 exploring with preserved app data." : "App data reset unavailable (platform/device policy) \u2014 exploring the app in its current state.");
315852
+ const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
315853
+ log2(exploreAppDataCleared ? "App data cleared \u2014 exploring from a deterministic cold (logged-out) state." : target?.resetPolicy === "PRESERVE_APP_DATA" ? "App data reset skipped by policy \u2014 exploring with preserved app data." : mode === "survey" && (target?.resetPolicy ?? "AUTO") === "AUTO" ? "App data reset skipped for mobile survey AUTO \u2014 exploring with preserved app data. Use resetPolicy=CLEAR_APP_DATA for a cold logged-out survey." : "App data reset unavailable (platform/device policy) \u2014 exploring the app in its current state.");
315417
315854
  const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
315418
315855
  const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
315419
315856
  const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
@@ -315444,9 +315881,15 @@ var require_mobile_discovery_agent = __commonJS({
315444
315881
  log2(`
315445
315882
  Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
315446
315883
  if (callbacks?.onExploreComplete) {
315447
- await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions)).catch((err) => {
315448
- log2(`[stream] explore-complete callback failed: ${err instanceof Error ? err.message : String(err)}`);
315449
- });
315884
+ try {
315885
+ await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
315886
+ } catch (err) {
315887
+ const message = checkpointErrorMessage(err);
315888
+ log2(`[stream] explore-complete callback failed: ${message}`);
315889
+ if (isTerminalDiscoveryCheckpointError(err)) {
315890
+ throw err;
315891
+ }
315892
+ }
315450
315893
  }
315451
315894
  if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
315452
315895
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -315474,8 +315917,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315474
315917
  pausePhase: "explore"
315475
315918
  };
315476
315919
  }
315477
- log2("\nRelaunching app for a clean state before planning/generation...");
315478
- await driver.relaunch();
315920
+ if (!isSurvey) {
315921
+ log2("\nRelaunching app for a clean state before planning/generation...");
315922
+ await driver.relaunch();
315923
+ }
315479
315924
  log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
315480
315925
  const graph = state2.toScreenGraph();
315481
315926
  const screenMap = state2.buildInMemoryScreenMap();
@@ -315498,7 +315943,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315498
315943
  hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
315499
315944
  },
315500
315945
  incremental: ctx.incremental,
315501
- testBudget
315946
+ testBudget,
315947
+ userNotes: ctx.userNotes,
315948
+ featureContext: ctx.featureContext
315502
315949
  }, {
315503
315950
  modelOverride: ctx.planModel ?? ctx.generateModel,
315504
315951
  onAICall
@@ -315655,10 +316102,11 @@ Fatal error: ${msg}`);
315655
316102
  };
315656
316103
  }
315657
316104
  }
315658
- async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
316105
+ async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
315659
316106
  void phase;
315660
316107
  const resetPolicy = target?.resetPolicy ?? "AUTO";
315661
- if (resetPolicy === "PRESERVE_APP_DATA") {
316108
+ const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
316109
+ if (shouldPreserveAppData) {
315662
316110
  try {
315663
316111
  await driver.relaunch();
315664
316112
  } catch (err) {
@@ -315670,10 +316118,20 @@ Fatal error: ${msg}`);
315670
316118
  log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
315671
316119
  return false;
315672
316120
  }) ?? false;
315673
- if (!cleared && resetPolicy === "CLEAR_APP_DATA") {
315674
- log2("CLEAR_APP_DATA requested but app-data reset was unavailable; continuing without clearing app data.");
316121
+ if (cleared)
316122
+ return true;
316123
+ if (resetPolicy === "CLEAR_APP_DATA") {
316124
+ log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
316125
+ }
316126
+ if (mode !== "survey") {
316127
+ return false;
315675
316128
  }
315676
- return cleared;
316129
+ try {
316130
+ await driver.relaunch();
316131
+ } catch (err) {
316132
+ log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
316133
+ }
316134
+ return false;
315677
316135
  }
315678
316136
  function shouldPauseAfterTruncatedExplore(explore) {
315679
316137
  return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
@@ -315720,6 +316178,7 @@ var require_discover_agent = __commonJS({
315720
316178
  var claude_discovery_runner_js_1 = require_claude_discovery_runner();
315721
316179
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
315722
316180
  var canvas_detector_js_1 = require_canvas_detector();
316181
+ var auth_recipe_js_1 = require_auth_recipe();
315723
316182
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
315724
316183
  const cached = ctx.appRenderType ?? null;
315725
316184
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -316415,7 +316874,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316415
316874
  }
316416
316875
  if (envRefs.size > 0) {
316417
316876
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316418
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
316877
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316419
316878
  if (missing.length > 0) {
316420
316879
  autoVariableDefaults = {};
316421
316880
  for (const key of missing) {
@@ -316931,7 +317390,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316931
317390
  envRefs.add(match[1]);
316932
317391
  if (envRefs.size > 0) {
316933
317392
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316934
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
317393
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316935
317394
  if (missing.length > 0) {
316936
317395
  autoVariableDefaults = {};
316937
317396
  for (const key of missing) {
@@ -329813,9 +330272,9 @@ var require_dist2 = __commonJS({
329813
330272
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
329814
330273
  };
329815
330274
  Object.defineProperty(exports2, "__esModule", { value: true });
329816
- exports2.buildImportSystemRules = exports2.buildImportKickoff = exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
329817
- exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = void 0;
329818
- exports2.UNSUPPORTED_RENDERER_PREFIX = void 0;
330275
+ exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
330276
+ exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = void 0;
330277
+ exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = void 0;
329819
330278
  __exportStar(require_ai_provider(), exports2);
329820
330279
  __exportStar(require_mobile_types(), exports2);
329821
330280
  __exportStar(require_mobile_source_parser(), exports2);
@@ -329952,6 +330411,12 @@ var require_dist2 = __commonJS({
329952
330411
  Object.defineProperty(exports2, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
329953
330412
  return auth_recipe_js_1.AUTH_MARKER_VERSION;
329954
330413
  } });
330414
+ Object.defineProperty(exports2, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
330415
+ return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
330416
+ } });
330417
+ Object.defineProperty(exports2, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
330418
+ return auth_recipe_js_1.isReservedRunnerEnvVar;
330419
+ } });
329955
330420
  var auth_capture_run_js_1 = require_auth_capture_run();
329956
330421
  Object.defineProperty(exports2, "runLoginCaptureRun", { enumerable: true, get: function() {
329957
330422
  return auth_capture_run_js_1.runLoginCaptureRun;
@@ -330179,7 +330644,7 @@ var require_package4 = __commonJS({
330179
330644
  "package.json"(exports2, module2) {
330180
330645
  module2.exports = {
330181
330646
  name: "@validate.qa/runner",
330182
- version: "1.0.31",
330647
+ version: "1.0.33",
330183
330648
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
330184
330649
  bin: {
330185
330650
  "validate-runner": "dist/cli.js"
@@ -330795,25 +331260,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
330795
331260
  }
330796
331261
  return elementId;
330797
331262
  }
331263
+ async function sendKeysToFocusedInput(sessionPath, value) {
331264
+ await appiumRequest(`${sessionPath}/keys`, {
331265
+ method: "POST",
331266
+ body: JSON.stringify({ text: value, value: Array.from(value) })
331267
+ });
331268
+ }
330798
331269
  async function typeText(sessionPath, value, bounds) {
330799
331270
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330800
- if (!elementId) {
330801
- throw new Error("No focused element available for TYPE. Tap an input field first.");
331271
+ if (elementId) {
331272
+ try {
331273
+ await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
331274
+ method: "POST",
331275
+ body: JSON.stringify({ text: value })
331276
+ });
331277
+ return;
331278
+ } catch (err) {
331279
+ if (isAppiumDriverDeadError(err)) throw err;
331280
+ }
330802
331281
  }
330803
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
330804
- method: "POST",
330805
- body: JSON.stringify({ text: value })
330806
- });
331282
+ await sendKeysToFocusedInput(sessionPath, value);
330807
331283
  }
330808
331284
  async function clearField(sessionPath, bounds) {
330809
331285
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330810
- if (!elementId) {
330811
- throw new Error("No focused element available for CLEAR. Tap an input field first.");
331286
+ if (elementId) {
331287
+ try {
331288
+ await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
331289
+ method: "POST",
331290
+ body: JSON.stringify({})
331291
+ });
331292
+ return;
331293
+ } catch (err) {
331294
+ if (isAppiumDriverDeadError(err)) throw err;
331295
+ }
331296
+ }
331297
+ try {
331298
+ await appiumRequest(`${sessionPath}/keys`, {
331299
+ method: "POST",
331300
+ // Common select-all + delete sequence is platform-dependent; empty keys
331301
+ // after focus is the portable baseline Appium accepts for many hybrids.
331302
+ body: JSON.stringify({ text: "", value: [] })
331303
+ });
331304
+ } catch (err) {
331305
+ if (isAppiumDriverDeadError(err)) throw err;
331306
+ throw new Error(
331307
+ `No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
331308
+ );
330812
331309
  }
330813
- await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
330814
- method: "POST",
330815
- body: JSON.stringify({})
330816
- });
330817
331310
  }
330818
331311
  async function swipe(sessionPath, direction, distance, windowSize) {
330819
331312
  const dist = clampMobileSwipeDistance(distance);
@@ -330843,6 +331336,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
330843
331336
  async function pressBack(sessionPath) {
330844
331337
  await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
330845
331338
  }
331339
+ function isKeyboardAlreadyHiddenError(err) {
331340
+ const message = err instanceof Error ? err.message : String(err);
331341
+ return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
331342
+ }
331343
+ async function dismissKeyboard(sessionPath, platform3) {
331344
+ if (platform3 === "ANDROID") {
331345
+ await pressBack(sessionPath);
331346
+ return;
331347
+ }
331348
+ try {
331349
+ await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
331350
+ method: "POST",
331351
+ body: "{}"
331352
+ });
331353
+ return;
331354
+ } catch (err) {
331355
+ if (isAppiumDriverDeadError(err)) throw err;
331356
+ if (isKeyboardAlreadyHiddenError(err)) return;
331357
+ }
331358
+ try {
331359
+ await appiumRequest(`${sessionPath}/execute/sync`, {
331360
+ method: "POST",
331361
+ body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
331362
+ });
331363
+ return;
331364
+ } catch (err) {
331365
+ if (isAppiumDriverDeadError(err)) throw err;
331366
+ if (isKeyboardAlreadyHiddenError(err)) return;
331367
+ }
331368
+ await pressBack(sessionPath);
331369
+ }
330846
331370
  async function pressHome(sessionPath, platform3) {
330847
331371
  if (platform3 === "IOS") {
330848
331372
  await appiumRequest(`${sessionPath}/execute/sync`, {
@@ -332020,6 +332544,10 @@ async function startMobileNetworkCapture(options) {
332020
332544
 
332021
332545
  // src/services/appium-executor.ts
332022
332546
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
332547
+ var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
332548
+ function validateElementId(elementId) {
332549
+ return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
332550
+ }
332023
332551
  var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
332024
332552
  const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
332025
332553
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
@@ -332154,6 +332682,9 @@ async function createSession(target) {
332154
332682
  });
332155
332683
  const sessionId = result?.value?.sessionId ?? result?.sessionId;
332156
332684
  if (!sessionId) throw new Error("Appium session creation returned no session ID");
332685
+ if (target.platform === "ANDROID") {
332686
+ await prepareAndroidInputEnvironment(target.deviceId);
332687
+ }
332157
332688
  return sessionId;
332158
332689
  }
332159
332690
  var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
@@ -332541,7 +333072,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
332541
333072
  method: "POST",
332542
333073
  body: JSON.stringify({ using: strategy.using, value: strategy.value })
332543
333074
  });
332544
- const elementId = result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"];
333075
+ const elementId = validateElementId(
333076
+ result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
333077
+ );
332545
333078
  if (elementId) return elementId;
332546
333079
  } catch (err) {
332547
333080
  if (isTransportError(err)) throw err;
@@ -332581,24 +333114,25 @@ async function tapCoordinates(sessionId, bounds) {
332581
333114
  })
332582
333115
  });
332583
333116
  }
332584
- async function getActiveElementId2(sessionId) {
332585
- try {
332586
- const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
332587
- return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
332588
- } catch {
332589
- return null;
332590
- }
332591
- }
332592
- async function sendKeysToFocusedInput(sessionId, value) {
332593
- await wdRequest(`/session/${sessionId}/keys`, {
332594
- method: "POST",
332595
- body: JSON.stringify({ text: value, value: Array.from(value) })
332596
- });
332597
- }
332598
333117
  function encodeAndroidInputText(value) {
332599
333118
  const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
332600
333119
  return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
332601
333120
  }
333121
+ async function prepareAndroidInputEnvironment(deviceId) {
333122
+ if (!deviceId) return;
333123
+ const settingsToDisable = [
333124
+ ["secure", "stylus_handwriting_enabled", "0"]
333125
+ ];
333126
+ for (const [namespace, key, value] of settingsToDisable) {
333127
+ try {
333128
+ await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
333129
+ timeout: 8e3,
333130
+ windowsHide: true
333131
+ });
333132
+ } catch {
333133
+ }
333134
+ }
333135
+ }
332602
333136
  async function sendAndroidTextToFocusedInput(deviceId, value) {
332603
333137
  if (!deviceId) {
332604
333138
  throw new Error("Android keyboard fallback requires a device id");
@@ -332660,100 +333194,28 @@ async function executeType(sessionId, step, context) {
332660
333194
  if (step.value === void 0 || step.value === null || step.value === "") return;
332661
333195
  const value = String(step.value);
332662
333196
  const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
332663
- let elementId = null;
332664
- let lastInputError = null;
332665
- let tappedBounds = false;
332666
- if (bounds) {
332667
- await tapCoordinates(sessionId, bounds);
332668
- tappedBounds = true;
332669
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332670
- elementId = await getActiveElementId2(sessionId);
332671
- if (elementId) {
332672
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332673
- if (!lastInputError) return;
332674
- elementId = null;
332675
- }
332676
- }
332677
- if (step.target) {
332678
- try {
332679
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
332680
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332681
- if (!lastInputError) return;
332682
- elementId = null;
332683
- } catch (error2) {
332684
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
332685
- lastInputError = error2;
332686
- }
332687
- }
332688
- if (!elementId && bounds) {
332689
- await tapCoordinates(sessionId, bounds);
332690
- tappedBounds = true;
332691
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332692
- elementId = await getActiveElementId2(sessionId);
332693
- }
332694
- if (!elementId) {
332695
- elementId = await getActiveElementId2(sessionId);
332696
- }
332697
- if (elementId) {
332698
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332699
- if (!lastInputError) return;
332700
- elementId = null;
332701
- if (!bounds) {
332702
- throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
332703
- }
332704
- }
332705
- if (bounds) {
332706
- if (!tappedBounds) {
332707
- await tapCoordinates(sessionId, bounds);
332708
- tappedBounds = true;
332709
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332710
- }
332711
- try {
332712
- await sendKeysToFocusedInput(sessionId, value);
332713
- return;
332714
- } catch (error2) {
332715
- if (isTransportError(error2)) throw error2;
332716
- lastInputError = error2;
332717
- }
332718
- }
332719
- if (context.platform === "ANDROID" && tappedBounds) {
332720
- try {
332721
- context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
332722
- await sendAndroidTextToFocusedInput(context.deviceId, value);
332723
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332724
- return;
332725
- } catch (error2) {
332726
- if (isTransportError(error2)) throw error2;
332727
- lastInputError = error2;
333197
+ const sessionPath = `/session/${sessionId}`;
333198
+ try {
333199
+ await typeText(sessionPath, value, bounds);
333200
+ return;
333201
+ } catch (error2) {
333202
+ if (isTransportError(error2)) throw error2;
333203
+ if (context.platform === "ANDROID" && bounds) {
333204
+ try {
333205
+ context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
333206
+ await sendAndroidTextToFocusedInput(context.deviceId, value);
333207
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
333208
+ return;
333209
+ } catch (androidError) {
333210
+ if (isTransportError(androidError)) throw androidError;
333211
+ }
332728
333212
  }
333213
+ throw error2 instanceof Error ? error2 : new Error(String(error2));
332729
333214
  }
332730
- throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
332731
333215
  }
332732
333216
  async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
332733
333217
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
332734
- let elementId = null;
332735
- if (step.target) {
332736
- try {
332737
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
332738
- } catch (error2) {
332739
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
332740
- }
332741
- }
332742
- if (!elementId && bounds) {
332743
- await tapCoordinates(sessionId, bounds);
332744
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332745
- elementId = await getActiveElementId2(sessionId);
332746
- }
332747
- if (!elementId) {
332748
- elementId = await getActiveElementId2(sessionId);
332749
- }
332750
- if (!elementId) {
332751
- throw new Error("CLEAR step could not resolve a target or focused element");
332752
- }
332753
- await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
332754
- method: "POST",
332755
- body: "{}"
332756
- });
333218
+ await clearField(`/session/${sessionId}`, bounds);
332757
333219
  }
332758
333220
  async function executeSwipe(sessionId, step) {
332759
333221
  const direction = step.metadata?.direction || "up";
@@ -334836,6 +335298,10 @@ var MobileBridgeDriver = class {
334836
335298
  async back() {
334837
335299
  return this.runAction(() => pressBack(this.sessionPath));
334838
335300
  }
335301
+ /** Platform-aware software keyboard dismissal. */
335302
+ async dismissKeyboard() {
335303
+ return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
335304
+ }
334839
335305
  /** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
334840
335306
  async home() {
334841
335307
  return this.runAction(() => pressHome(this.sessionPath, this.platform));
@@ -335221,8 +335687,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
335221
335687
  pageUrl: call.pageUrl ?? pageUrl
335222
335688
  }));
335223
335689
  }
335690
+ var runnerPackage = require_package4();
335224
335691
  var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
335225
335692
  var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
335693
+ var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
335694
+ var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
335695
+ var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION;
335226
335696
  var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
335227
335697
  var DEFAULT_TYPE_LIMITS = {
335228
335698
  discovery: IS_CLOUD ? 2 : 15,
@@ -338056,6 +338526,9 @@ async function startRunner(opts) {
338056
338526
  method: "POST",
338057
338527
  headers,
338058
338528
  body: JSON.stringify({
338529
+ runnerName: RUNNER_NAME,
338530
+ runnerVersion: RUNNER_VERSION,
338531
+ runnerBuildId: RUNNER_BUILD_ID,
338059
338532
  runsCompleted,
338060
338533
  activeRuns,
338061
338534
  maxConcurrent: MAX_CONCURRENT,