@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.mjs CHANGED
@@ -5006,6 +5006,29 @@ var require_mobile_observation = __commonJS({
5006
5006
  }
5007
5007
  });
5008
5008
 
5009
+ // ../runner-core/dist/services/mobile/mobile-keyboard.js
5010
+ var require_mobile_keyboard = __commonJS({
5011
+ "../runner-core/dist/services/mobile/mobile-keyboard.js"(exports) {
5012
+ "use strict";
5013
+ Object.defineProperty(exports, "__esModule", { value: true });
5014
+ exports.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
5015
+ exports.dismissKeyboardForRetry = dismissKeyboardForRetry;
5016
+ var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
5017
+ var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
5018
+ function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
5019
+ if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
5020
+ return false;
5021
+ return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
5022
+ }
5023
+ async function dismissKeyboardForRetry(driver) {
5024
+ if (typeof driver.dismissKeyboard === "function") {
5025
+ return driver.dismissKeyboard();
5026
+ }
5027
+ return driver.back();
5028
+ }
5029
+ }
5030
+ });
5031
+
5009
5032
  // ../runner-core/dist/utils/scrub-credentials.js
5010
5033
  var require_scrub_credentials = __commonJS({
5011
5034
  "../runner-core/dist/utils/scrub-credentials.js"(exports) {
@@ -61987,6 +62010,7 @@ var require_mobile_explorer = __commonJS({
61987
62010
  var mobile_source_parser_js_1 = require_mobile_source_parser();
61988
62011
  var mobile_observation_js_1 = require_mobile_observation();
61989
62012
  var mobile_boundary_js_1 = require_mobile_boundary();
62013
+ var mobile_keyboard_js_1 = require_mobile_keyboard();
61990
62014
  var credential_tools_js_1 = require_credential_tools();
61991
62015
  var verification_inbox_js_1 = require_verification_inbox();
61992
62016
  var inbox_tools_js_1 = require_inbox_tools();
@@ -61996,6 +62020,7 @@ var require_mobile_explorer = __commonJS({
61996
62020
  var MAX_RECENT_EXCHANGES = 24;
61997
62021
  var OBSERVATION_MAX_ELEMENTS = 50;
61998
62022
  var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
62023
+ var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
61999
62024
  var MOBILE_SNAPSHOT_TOOL = {
62000
62025
  type: "function",
62001
62026
  function: {
@@ -62265,7 +62290,7 @@ var require_mobile_explorer = __commonJS({
62265
62290
  return { screen, signal, refMap, observation };
62266
62291
  }
62267
62292
  function buildStatePacket(input) {
62268
- const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
62293
+ const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
62269
62294
  const sections = [];
62270
62295
  sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
62271
62296
  sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
@@ -62278,10 +62303,46 @@ var require_mobile_explorer = __commonJS({
62278
62303
  sections.push("");
62279
62304
  if (mode === "survey") {
62280
62305
  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.");
62306
+ } else if (featureName?.trim()) {
62307
+ 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.`);
62281
62308
  } else {
62282
62309
  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.");
62283
62310
  }
62284
62311
  sections.push("");
62312
+ if (mode === "deep" && featureName?.trim()) {
62313
+ sections.push(`## Feature: ${featureName.trim()}`);
62314
+ if (featureContext?.plannerDescription?.trim()) {
62315
+ sections.push(featureContext.plannerDescription.trim());
62316
+ }
62317
+ if (featureContext?.priorInsight?.trim()) {
62318
+ sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
62319
+ }
62320
+ if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
62321
+ sections.push("### Target screens / routes for this feature");
62322
+ for (const page of featureContext.pageIntel.slice(0, 12)) {
62323
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
62324
+ }
62325
+ }
62326
+ if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
62327
+ sections.push("### Known flows");
62328
+ for (const flow of featureContext.flowIntel.slice(0, 8)) {
62329
+ sections.push(`- ${flow}`);
62330
+ }
62331
+ }
62332
+ if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
62333
+ sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
62334
+ for (const other of featureContext.otherFeatures.slice(0, 12)) {
62335
+ const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
62336
+ sections.push(`- ${other.name}${pages}`);
62337
+ }
62338
+ }
62339
+ sections.push("");
62340
+ }
62341
+ if (userNotes?.trim()) {
62342
+ sections.push(`## Operator notes
62343
+ ${userNotes.trim()}`);
62344
+ sections.push("");
62345
+ }
62285
62346
  const screenCount = state2.getScreenCount();
62286
62347
  sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
62287
62348
  const currentScreenId = state2.getCurrentScreenId();
@@ -62438,7 +62499,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62438
62499
  } catch {
62439
62500
  }
62440
62501
  };
62441
- const mode = ctx.mode === "deep" ? "deep" : "survey";
62502
+ const mode = ctx.mode === "survey" ? "survey" : "deep";
62442
62503
  const platform3 = platformForMedium(ctx.medium);
62443
62504
  const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
62444
62505
  const targetAppId = ctx.target?.appId?.trim() || void 0;
@@ -62561,9 +62622,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62561
62622
  externalOwner: boundary.owner
62562
62623
  };
62563
62624
  }
62564
- log2(` Relaunch still outside target app (${recoveredBoundary.owner}); no screen recorded.`);
62625
+ let lastExternalOwner = recoveredBoundary.owner;
62626
+ for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
62627
+ try {
62628
+ await driver.launchApp();
62629
+ const retrySnap = await driver.snapshot();
62630
+ const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
62631
+ if (!retryRecovered)
62632
+ continue;
62633
+ const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
62634
+ latest = retryRecovered;
62635
+ lastSnapshotIteration = iterationForObservation;
62636
+ if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
62637
+ const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
62638
+ captureScreenshot(retrySnap.screenshot, screenId);
62639
+ log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
62640
+ return {
62641
+ observation: retryRecovered.observation,
62642
+ screenId,
62643
+ externalBlocked: true,
62644
+ externalOwner: boundary.owner
62645
+ };
62646
+ }
62647
+ if (retryBoundary.kind === "transient_external") {
62648
+ log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
62649
+ return {
62650
+ observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
62651
+ screenId: null,
62652
+ externalBlocked: true,
62653
+ transientExternal: true,
62654
+ externalOwner: boundary.owner
62655
+ };
62656
+ }
62657
+ lastExternalOwner = retryBoundary.owner;
62658
+ } catch (err) {
62659
+ log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
62660
+ }
62661
+ }
62662
+ log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
62565
62663
  return {
62566
- observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no screen was recorded)`,
62664
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
62567
62665
  screenId: null,
62568
62666
  externalBlocked: true,
62569
62667
  externalOwner: boundary.owner
@@ -62689,7 +62787,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62689
62787
  maxIterations,
62690
62788
  latestObservation: getLatest()?.observation ?? null,
62691
62789
  journeys,
62692
- transitions: collectedTransitions
62790
+ transitions: collectedTransitions,
62791
+ featureName: ctx.featureName,
62792
+ featureContext: ctx.featureContext,
62793
+ userNotes: ctx.userNotes
62693
62794
  });
62694
62795
  const messages = [
62695
62796
  { role: "system", content: systemPrompt },
@@ -62941,7 +63042,19 @@ Exploration complete: ${summary}`);
62941
63042
  const fromScreenId = state2.getCurrentScreenId();
62942
63043
  const boundaryBeforeAction = currentBoundary();
62943
63044
  const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
62944
- const actionResult = await driver.tap(resolved.bounds);
63045
+ let actionResult = await driver.tap(resolved.bounds);
63046
+ if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
63047
+ log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
63048
+ const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
63049
+ const retryResult = await driver.tap(resolved.bounds);
63050
+ actionResult = retryResult.ok ? retryResult : {
63051
+ ...retryResult,
63052
+ error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
63053
+ source: retryResult.source ?? dismissResult.source,
63054
+ screenshot: retryResult.screenshot ?? dismissResult.screenshot,
63055
+ screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
63056
+ };
63057
+ }
62945
63058
  const absorbed = await absorbActionResult(actionResult, "mobile_tap");
62946
63059
  const { observation, screenId } = absorbed;
62947
63060
  if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
@@ -65941,6 +66054,25 @@ var require_mobile_discovery_prompts = __commonJS({
65941
66054
  function targetAppScreens(graph, appId) {
65942
66055
  return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
65943
66056
  }
66057
+ function isGenericNonFeatureScreen(state2) {
66058
+ const type = state2.screenType?.trim().toLowerCase();
66059
+ if (type === "error" || type === "permission")
66060
+ return true;
66061
+ if (type && type !== "unknown")
66062
+ return false;
66063
+ const actionableCount = state2.elements.filter((el) => el.actionable).length;
66064
+ if (actionableCount > 0)
66065
+ return false;
66066
+ const evidenceText = [
66067
+ state2.screenName,
66068
+ state2.screenType,
66069
+ ...state2.observations ?? []
66070
+ ].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
66071
+ return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
66072
+ }
66073
+ function plannableTargetAppScreens(graph, appId) {
66074
+ return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
66075
+ }
65944
66076
  exports.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.
65945
66077
 
65946
66078
  ## Output Discipline
@@ -65960,8 +66092,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
65960
66092
  5. CRUD screens for the same entity belong together
65961
66093
  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).
65962
66094
  7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
65963
- 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.
65964
- 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.
66095
+ 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.
66096
+ 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.
65965
66097
  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.
65966
66098
 
65967
66099
  ## Output JSON Schema
@@ -66227,8 +66359,36 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
66227
66359
  if (context.featureName) {
66228
66360
  sections.push(`## Feature: ${context.featureName}
66229
66361
  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.`);
66362
+ if (context.featureContext?.plannerDescription?.trim()) {
66363
+ sections.push(context.featureContext.plannerDescription.trim());
66364
+ }
66365
+ if (context.featureContext?.priorInsight?.trim()) {
66366
+ sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
66367
+ }
66368
+ if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
66369
+ sections.push("### Feature page intel");
66370
+ for (const page of context.featureContext.pageIntel.slice(0, 12)) {
66371
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
66372
+ }
66373
+ }
66374
+ if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
66375
+ sections.push("### Known flows");
66376
+ for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
66377
+ sections.push(`- ${flow}`);
66378
+ }
66379
+ }
66380
+ if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
66381
+ sections.push("### Other features (do not plan primary scenarios for these)");
66382
+ for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
66383
+ sections.push(`- ${other.name}`);
66384
+ }
66385
+ }
66386
+ }
66387
+ if (context.userNotes?.trim()) {
66388
+ sections.push(`## Operator notes
66389
+ ${context.userNotes.trim()}`);
66230
66390
  }
66231
- const orderedScreens = targetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66391
+ const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66232
66392
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
66233
66393
  sections.push(`## Screens Discovered (${orderedScreens.length})`);
66234
66394
  for (const state2 of orderedScreens) {
@@ -66337,7 +66497,7 @@ ${constraints.join("\n")}`);
66337
66497
  const now = /* @__PURE__ */ new Date();
66338
66498
  const idByScreen = /* @__PURE__ */ new Map();
66339
66499
  let pageSeq = 0;
66340
- const orderedScreens = targetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66500
+ const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66341
66501
  for (const state2 of orderedScreens) {
66342
66502
  idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
66343
66503
  }
@@ -66353,7 +66513,7 @@ ${constraints.join("\n")}`);
66353
66513
  routePattern: null,
66354
66514
  title: state2.screenName ?? null,
66355
66515
  pageType: state2.screenType ?? "UNKNOWN",
66356
- requiresAuth: false,
66516
+ requiresAuth: state2.screenType?.toLowerCase() === "auth",
66357
66517
  visitCount: state2.visited ? 1 : 0,
66358
66518
  lastVisitedAt: state2.visited ? now : null,
66359
66519
  elementsCovered: 0,
@@ -66373,6 +66533,8 @@ ${constraints.join("\n")}`);
66373
66533
  const toPageId = idByScreen.get(t.toScreenId);
66374
66534
  if (!fromPageId || !toPageId)
66375
66535
  continue;
66536
+ const fromScreen = graph.screens.get(t.fromScreenId);
66537
+ const toScreen = graph.screens.get(t.toScreenId);
66376
66538
  transitions.push({
66377
66539
  id: `mtx-${txSeq++}`,
66378
66540
  fromPageId,
@@ -66380,7 +66542,7 @@ ${constraints.join("\n")}`);
66380
66542
  triggerType: t.action.toUpperCase(),
66381
66543
  triggerLabel: t.viaRef ?? null,
66382
66544
  observedCount: 1,
66383
- requiresAuth: false
66545
+ requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
66384
66546
  });
66385
66547
  }
66386
66548
  return {
@@ -66398,7 +66560,7 @@ ${constraints.join("\n")}`);
66398
66560
  }
66399
66561
  function buildMobileFeatureGroupsFromGraph(graph, appId) {
66400
66562
  const groups = /* @__PURE__ */ new Map();
66401
- const orderedScreens = targetAppScreens(graph, appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66563
+ const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66402
66564
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
66403
66565
  for (const state2 of orderedScreens) {
66404
66566
  const screenTypeLower = state2.screenType?.toLowerCase();
@@ -66445,7 +66607,7 @@ ${constraints.join("\n")}`);
66445
66607
  return [];
66446
66608
  if (input.featureName?.trim())
66447
66609
  return [];
66448
- 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);
66610
+ const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66449
66611
  if (orderedScreens.length === 0)
66450
66612
  return [];
66451
66613
  const entryScreenId = orderedScreens[0].screenId;
@@ -66583,7 +66745,9 @@ ${constraints.join("\n")}`);
66583
66745
  recordedJourneys,
66584
66746
  credentials: input.credentials,
66585
66747
  testBudget: input.testBudget,
66586
- incremental: input.incremental
66748
+ incremental: input.incremental,
66749
+ userNotes: input.userNotes,
66750
+ featureContext: input.featureContext
66587
66751
  })
66588
66752
  });
66589
66753
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
@@ -66647,9 +66811,11 @@ var require_mobile_generator = __commonJS({
66647
66811
  var mobile_source_parser_js_2 = require_mobile_source_parser();
66648
66812
  var mobile_observation_js_1 = require_mobile_observation();
66649
66813
  var mobile_boundary_js_1 = require_mobile_boundary();
66814
+ var mobile_keyboard_js_1 = require_mobile_keyboard();
66650
66815
  var MAX_SCENARIO_ITERATIONS = 30;
66651
66816
  var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
66652
66817
  var MAX_RECENT_EXCHANGES = 24;
66818
+ var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
66653
66819
  var OBSERVATION_MAX_ELEMENTS = 50;
66654
66820
  function finalizeAction(input) {
66655
66821
  return { ...input, url: "", selector: input.selector ?? "" };
@@ -66840,8 +67006,38 @@ var require_mobile_generator = __commonJS({
66840
67006
  return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
66841
67007
  }
66842
67008
  var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
67009
+ 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;
66843
67010
  function isEditableMobileElement(element) {
66844
- return EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim());
67011
+ if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
67012
+ return true;
67013
+ const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67014
+ if (identity && EDITABLE_IDENTITY_RE.test(identity))
67015
+ return true;
67016
+ return false;
67017
+ }
67018
+ function isIdentityBearingTapTarget(element) {
67019
+ if (weakAssertionTargetReason(element))
67020
+ return false;
67021
+ if (isBareContainerLikeElement(element))
67022
+ return false;
67023
+ if (isEditableMobileElement(element))
67024
+ return false;
67025
+ if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
67026
+ return true;
67027
+ const label = [
67028
+ element.label,
67029
+ element.text,
67030
+ element.accessibilityId,
67031
+ element.resourceId
67032
+ ].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67033
+ 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);
67034
+ }
67035
+ function isBareContainerLikeElement(element) {
67036
+ if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
67037
+ const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
67038
+ return !hasIdentity;
67039
+ }
67040
+ return false;
66845
67041
  }
66846
67042
  function nonEmpty2(value) {
66847
67043
  if (typeof value !== "string")
@@ -67065,10 +67261,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
67065
67261
  externalOwner: boundary.owner
67066
67262
  };
67067
67263
  }
67068
- log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
67264
+ let lastExternalOwner = recoveredBoundary.owner;
67265
+ let lastRecovered = recovered;
67266
+ for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
67267
+ try {
67268
+ await driver.launchApp();
67269
+ const retrySnap = await driver.snapshot();
67270
+ const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
67271
+ if (!retryRecovered)
67272
+ continue;
67273
+ lastRecovered = retryRecovered;
67274
+ const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
67275
+ setLatest(retryRecovered);
67276
+ if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
67277
+ emitScreenshot(retrySnap.screenshot);
67278
+ log2(` recovered target app after ${attempt} additional activation attempt(s).`);
67279
+ return {
67280
+ resolved: retryRecovered,
67281
+ observation: retryRecovered.observation,
67282
+ externalBlocked: true,
67283
+ externalOwner: boundary.owner
67284
+ };
67285
+ }
67286
+ if (retryBoundary.kind === "transient_external") {
67287
+ log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
67288
+ return {
67289
+ resolved: retryRecovered,
67290
+ observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
67291
+ externalBlocked: true,
67292
+ transientExternal: true,
67293
+ externalOwner: boundary.owner
67294
+ };
67295
+ }
67296
+ lastExternalOwner = retryBoundary.owner;
67297
+ } catch (err) {
67298
+ log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
67299
+ }
67300
+ }
67301
+ log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
67069
67302
  return {
67070
- resolved: recovered,
67071
- observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
67303
+ resolved: lastRecovered,
67304
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
67072
67305
  externalBlocked: true,
67073
67306
  externalOwner: boundary.owner
67074
67307
  };
@@ -67282,9 +67515,26 @@ ${statePacket}` },
67282
67515
  return unknownRef(toolArgs.ref);
67283
67516
  const beforeScreenName = screenName();
67284
67517
  const before = latest();
67285
- const result = await driver.tap(element.bounds);
67518
+ let result = await driver.tap(element.bounds);
67519
+ if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
67520
+ log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
67521
+ const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
67522
+ const retryResult = await driver.tap(element.bounds);
67523
+ result = retryResult.ok ? retryResult : {
67524
+ ...retryResult,
67525
+ error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
67526
+ source: retryResult.source ?? dismissResult.source,
67527
+ screenshot: retryResult.screenshot ?? dismissResult.screenshot,
67528
+ screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
67529
+ };
67530
+ }
67286
67531
  const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
67287
- if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
67532
+ const screenChanged = changedScreen(before, absorbed);
67533
+ const identityTap = isIdentityBearingTapTarget(element);
67534
+ if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
67535
+ if (!screenChanged && identityTap) {
67536
+ log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
67537
+ }
67288
67538
  recordAction({
67289
67539
  type: "TAP",
67290
67540
  selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
@@ -67428,6 +67678,13 @@ ${statePacket}` },
67428
67678
  const element = resolveRef(toolArgs.ref);
67429
67679
  if (!element)
67430
67680
  return unknownRef(toolArgs.ref);
67681
+ const weakReason = weakAssertionTargetReason(element);
67682
+ if (weakReason) {
67683
+ return {
67684
+ ok: false,
67685
+ 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.`
67686
+ };
67687
+ }
67431
67688
  const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
67432
67689
  if (!expected)
67433
67690
  return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
@@ -67686,19 +67943,46 @@ ${statePacket}` },
67686
67943
  if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
67687
67944
  return "auth/session scenario tapped a login or sign-in control without recording a credential path";
67688
67945
  }
67689
- if (!isCredentialSignIn)
67946
+ if (isCredentialSignIn) {
67947
+ const typedEmail = typedEmailIndexes.length > 0;
67948
+ const typedPassword = typedPasswordIndexes.length > 0;
67949
+ const tappedSubmit = submitAfterCredentials;
67950
+ if (typedEmail && typedPassword && tappedSubmit) {
67951
+ } else {
67952
+ const missing = [
67953
+ typedEmail ? null : "email/username entry",
67954
+ typedPassword ? null : "password entry",
67955
+ tappedSubmit ? null : "sign-in submit tap"
67956
+ ].filter((value) => value !== null);
67957
+ return `credential sign-in scenario did not record ${missing.join(", ")}`;
67958
+ }
67959
+ }
67960
+ const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
67961
+ if (!isHappyPath)
67690
67962
  return null;
67691
- const typedEmail = typedEmailIndexes.length > 0;
67692
- const typedPassword = typedPasswordIndexes.length > 0;
67693
- const tappedSubmit = submitAfterCredentials;
67694
- if (typedEmail && typedPassword && tappedSubmit)
67963
+ let lastTypeIndex = -1;
67964
+ let lastAssertIndex = -1;
67965
+ actions.forEach((action, index) => {
67966
+ if (action.type === "TYPE")
67967
+ lastTypeIndex = index;
67968
+ if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
67969
+ lastAssertIndex = index;
67970
+ });
67971
+ if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
67695
67972
  return null;
67696
- const missing = [
67697
- typedEmail ? null : "email/username entry",
67698
- typedPassword ? null : "password entry",
67699
- tappedSubmit ? null : "sign-in submit tap"
67700
- ].filter((value) => value !== null);
67701
- return `credential sign-in scenario did not record ${missing.join(", ")}`;
67973
+ const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
67974
+ const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
67975
+ const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
67976
+ const commitAfterType = actions.some((action, index) => {
67977
+ if (action.type !== "TAP" || index <= lastTypeIndex || index >= lastAssertIndex)
67978
+ return false;
67979
+ const textForAction = actionText(action);
67980
+ 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);
67981
+ });
67982
+ if (screensDiffer && !commitAfterType) {
67983
+ return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
67984
+ }
67985
+ return null;
67702
67986
  }
67703
67987
  function actionCycleKey(action) {
67704
67988
  return `${action.type}|${action.selector}|${action.value ?? ""}`;
@@ -68633,8 +68917,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
68633
68917
  function coerceSteps(value) {
68634
68918
  if (!Array.isArray(value) || value.length === 0)
68635
68919
  return void 0;
68636
- const allValid = value.every((step) => isRecord(step) && typeof step.action === "string" && typeof step.order === "number");
68637
- return allValid ? value : void 0;
68920
+ const steps = [];
68921
+ for (let i = 0; i < value.length; i++) {
68922
+ const step = value[i];
68923
+ if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
68924
+ return void 0;
68925
+ }
68926
+ const orderRaw = step.order;
68927
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
68928
+ steps.push({
68929
+ ...step,
68930
+ order,
68931
+ action: step.action
68932
+ });
68933
+ }
68934
+ return steps.map((step, order) => ({ ...step, order }));
68638
68935
  }
68639
68936
  function parseHealVerdict(content) {
68640
68937
  const cleaned = stripCodeFences(content);
@@ -72855,6 +73152,48 @@ var require_snapshot_observation = __commonJS({
72855
73152
  var PROMOTED_FORM_FIELD_CAP = 12;
72856
73153
  var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
72857
73154
  var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
73155
+ var DEEP_NONNAV_CAP_CENSUS = 16;
73156
+ var CENSUS_NAV_LINK_CAP = 60;
73157
+ var CENSUS_MIN_ACTION_GROUP = 3;
73158
+ var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
73159
+ function leadingActionVerb(label) {
73160
+ if (!label)
73161
+ return null;
73162
+ const m = label.trim().match(/[A-Za-z]+/);
73163
+ return m ? m[0].toLowerCase() : null;
73164
+ }
73165
+ function isNavLinkEntry(entry) {
73166
+ return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
73167
+ }
73168
+ function isRowRepeatedControl(element) {
73169
+ return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
73170
+ }
73171
+ function rowContainerScope(containerPath) {
73172
+ if (!containerPath)
73173
+ return null;
73174
+ const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
73175
+ return m ? m[1].trim() : containerPath.trim();
73176
+ }
73177
+ function containerScopeLabel(scope) {
73178
+ if (!scope)
73179
+ return null;
73180
+ const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
73181
+ if (landmarks.length > 0)
73182
+ return landmarks[landmarks.length - 1][1].trim();
73183
+ const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
73184
+ return segs.length ? segs[segs.length - 1] : null;
73185
+ }
73186
+ function rowIdentityKey(element) {
73187
+ const rc = element.enrichment?.rowContext;
73188
+ if (rc)
73189
+ return `rc:${rc}`;
73190
+ const cp = element.containerPath;
73191
+ if (cp) {
73192
+ const m = cp.match(/>\s*row:(.+)$/i);
73193
+ return m ? `row:${m[1].trim()}` : `cp:${cp}`;
73194
+ }
73195
+ return element.ref ? `ref:${element.ref}` : "unknown";
73196
+ }
72858
73197
  function isGroupedFormField(element) {
72859
73198
  if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
72860
73199
  return false;
@@ -72870,6 +73209,7 @@ var require_snapshot_observation = __commonJS({
72870
73209
  const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
72871
73210
  const artifact = page.snapshotArtifact;
72872
73211
  const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
73212
+ const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
72873
73213
  const activeScope = inferActiveSnapshotScope(page);
72874
73214
  const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
72875
73215
  const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
@@ -72883,10 +73223,17 @@ var require_snapshot_observation = __commonJS({
72883
73223
  });
72884
73224
  let interactive;
72885
73225
  if (options.mode === "survey") {
72886
- const navLinks = allInteractive.filter(({ element }) => element.elementType === "LINK" && element.triggersNavigation);
72887
- const others = allInteractive.filter(({ element }) => !(element.elementType === "LINK" && element.triggersNavigation));
73226
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73227
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
72888
73228
  const othersCap = richRender ? 6 : 4;
72889
73229
  interactive = [...navLinks, ...others.slice(0, othersCap)];
73230
+ } else if (rowActionCensus) {
73231
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73232
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
73233
+ interactive = [
73234
+ ...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
73235
+ ...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
73236
+ ];
72890
73237
  } else {
72891
73238
  interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
72892
73239
  }
@@ -72899,6 +73246,36 @@ var require_snapshot_observation = __commonJS({
72899
73246
  }
72900
73247
  }
72901
73248
  }
73249
+ if (rowActionCensus && interactive.length > 0) {
73250
+ const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
73251
+ const bestByVerb = /* @__PURE__ */ new Map();
73252
+ for (const entry of allInteractive) {
73253
+ if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
73254
+ continue;
73255
+ const verb = leadingActionVerb(qualifyElementLabel(entry.element));
73256
+ if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
73257
+ continue;
73258
+ if (shownVerbs.has(verb))
73259
+ continue;
73260
+ if (interactive.includes(entry))
73261
+ continue;
73262
+ const existing = bestByVerb.get(verb);
73263
+ if (!existing) {
73264
+ bestByVerb.set(verb, entry);
73265
+ continue;
73266
+ }
73267
+ if (existing.element.isDisabled && !entry.element.isDisabled)
73268
+ bestByVerb.set(verb, entry);
73269
+ }
73270
+ const swapIns = [...bestByVerb.values()];
73271
+ if (swapIns.length > 0) {
73272
+ const navShown = interactive.filter(isNavLinkEntry);
73273
+ const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
73274
+ const evictable = Math.min(swapIns.length, nonNavShown.length);
73275
+ const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
73276
+ interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
73277
+ }
73278
+ }
72902
73279
  const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
72903
73280
  const allForms = artifact?.formGroups ?? page.formGroups ?? [];
72904
73281
  const forms = allForms.slice(0, options.formLimit ?? 3);
@@ -73198,6 +73575,54 @@ var require_snapshot_observation = __commonJS({
73198
73575
  const hidden = allInteractive.length - interactive.length;
73199
73576
  if (hidden > 0) {
73200
73577
  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.`);
73578
+ if (rowActionCensus) {
73579
+ const shownSet = new Set(interactive.map((entry) => entry.element));
73580
+ const groups = /* @__PURE__ */ new Map();
73581
+ for (const entry of allInteractive) {
73582
+ const el = entry.element;
73583
+ if (!isRowRepeatedControl(el))
73584
+ continue;
73585
+ const verb = leadingActionVerb(qualifyElementLabel(el));
73586
+ if (!verb)
73587
+ continue;
73588
+ const role = (el.role ?? el.elementType ?? "control").toLowerCase();
73589
+ const scope = rowContainerScope(el.containerPath);
73590
+ const key = JSON.stringify([scope ?? "", role, verb]);
73591
+ let group = groups.get(key);
73592
+ if (!group) {
73593
+ group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
73594
+ groups.set(key, group);
73595
+ }
73596
+ const rowKey = rowIdentityKey(el);
73597
+ group.rows.add(rowKey);
73598
+ group.controls += 1;
73599
+ if (shownSet.has(el))
73600
+ group.shownRows.add(rowKey);
73601
+ if (el.isDisabled)
73602
+ group.disabledRows.add(rowKey);
73603
+ }
73604
+ const collapsed = [...groups.values()].map((g) => ({
73605
+ verb: g.verb,
73606
+ role: g.role,
73607
+ scopeLabel: g.scopeLabel,
73608
+ total: g.rows.size,
73609
+ shown: g.shownRows.size,
73610
+ disabled: g.disabledRows.size,
73611
+ controls: g.controls
73612
+ })).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
73613
+ const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
73614
+ const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
73615
+ return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
73616
+ });
73617
+ for (const g of collapsed) {
73618
+ const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
73619
+ const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
73620
+ const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
73621
+ const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
73622
+ const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
73623
+ 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.`);
73624
+ }
73625
+ }
73201
73626
  }
73202
73627
  }
73203
73628
  }
@@ -74220,13 +74645,14 @@ var require_auth_recipe = __commonJS({
74220
74645
  "../runner-core/dist/services/auth-recipe.js"(exports) {
74221
74646
  "use strict";
74222
74647
  Object.defineProperty(exports, "__esModule", { value: true });
74223
- exports.AUTH_MARKER_VERSION = void 0;
74648
+ exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
74224
74649
  exports.computeRecipeHash = computeRecipeHash;
74225
74650
  exports.buildAuthRecipe = buildAuthRecipe;
74226
74651
  exports.authProofLocator = authProofLocator;
74227
74652
  exports.urlProofPredicateExpr = urlProofPredicateExpr;
74228
74653
  exports.renderAuthProofAssertion = renderAuthProofAssertion;
74229
74654
  exports.renderAuthFn = renderAuthFn;
74655
+ exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
74230
74656
  exports.absoluteRoute = absoluteRoute;
74231
74657
  exports.renderReplayClickArrow = renderReplayClickArrow;
74232
74658
  exports.renderReplaySettleArrow = renderReplaySettleArrow;
@@ -74345,6 +74771,17 @@ var require_auth_recipe = __commonJS({
74345
74771
  lines.push(END_MARKER);
74346
74772
  return lines.join("\n");
74347
74773
  }
74774
+ exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
74775
+ "AUTH_FILE",
74776
+ "SHARED_AUTH_INJECT",
74777
+ "VALIDATE_RUN_ID",
74778
+ "VALIDATE_TASK_ARN",
74779
+ "VALIDATE_RUNNER_AUTH",
74780
+ "VALIDATE_SERVER_URL"
74781
+ ]);
74782
+ function isReservedRunnerEnvVar(key) {
74783
+ return key.startsWith("VALIDATE_") || exports.RESERVED_RUNNER_ENV_VARS.has(key);
74784
+ }
74348
74785
  function absoluteRoute(route2, baseUrl) {
74349
74786
  try {
74350
74787
  return new URL(route2, baseUrl).href;
@@ -315417,8 +315854,8 @@ var require_mobile_discovery_agent = __commonJS({
315417
315854
  };
315418
315855
  }
315419
315856
  log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
315420
- const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
315421
- 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.");
315857
+ const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
315858
+ 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.");
315422
315859
  const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
315423
315860
  const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
315424
315861
  const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
@@ -315449,9 +315886,15 @@ var require_mobile_discovery_agent = __commonJS({
315449
315886
  log2(`
315450
315887
  Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
315451
315888
  if (callbacks?.onExploreComplete) {
315452
- await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions)).catch((err) => {
315453
- log2(`[stream] explore-complete callback failed: ${err instanceof Error ? err.message : String(err)}`);
315454
- });
315889
+ try {
315890
+ await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
315891
+ } catch (err) {
315892
+ const message = checkpointErrorMessage(err);
315893
+ log2(`[stream] explore-complete callback failed: ${message}`);
315894
+ if (isTerminalDiscoveryCheckpointError(err)) {
315895
+ throw err;
315896
+ }
315897
+ }
315455
315898
  }
315456
315899
  if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
315457
315900
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -315479,8 +315922,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315479
315922
  pausePhase: "explore"
315480
315923
  };
315481
315924
  }
315482
- log2("\nRelaunching app for a clean state before planning/generation...");
315483
- await driver.relaunch();
315925
+ if (!isSurvey) {
315926
+ log2("\nRelaunching app for a clean state before planning/generation...");
315927
+ await driver.relaunch();
315928
+ }
315484
315929
  log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
315485
315930
  const graph = state2.toScreenGraph();
315486
315931
  const screenMap = state2.buildInMemoryScreenMap();
@@ -315503,7 +315948,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315503
315948
  hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
315504
315949
  },
315505
315950
  incremental: ctx.incremental,
315506
- testBudget
315951
+ testBudget,
315952
+ userNotes: ctx.userNotes,
315953
+ featureContext: ctx.featureContext
315507
315954
  }, {
315508
315955
  modelOverride: ctx.planModel ?? ctx.generateModel,
315509
315956
  onAICall
@@ -315660,10 +316107,11 @@ Fatal error: ${msg}`);
315660
316107
  };
315661
316108
  }
315662
316109
  }
315663
- async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
316110
+ async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
315664
316111
  void phase;
315665
316112
  const resetPolicy = target?.resetPolicy ?? "AUTO";
315666
- if (resetPolicy === "PRESERVE_APP_DATA") {
316113
+ const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
316114
+ if (shouldPreserveAppData) {
315667
316115
  try {
315668
316116
  await driver.relaunch();
315669
316117
  } catch (err) {
@@ -315675,10 +316123,20 @@ Fatal error: ${msg}`);
315675
316123
  log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
315676
316124
  return false;
315677
316125
  }) ?? false;
315678
- if (!cleared && resetPolicy === "CLEAR_APP_DATA") {
315679
- log2("CLEAR_APP_DATA requested but app-data reset was unavailable; continuing without clearing app data.");
316126
+ if (cleared)
316127
+ return true;
316128
+ if (resetPolicy === "CLEAR_APP_DATA") {
316129
+ log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
316130
+ }
316131
+ if (mode !== "survey") {
316132
+ return false;
315680
316133
  }
315681
- return cleared;
316134
+ try {
316135
+ await driver.relaunch();
316136
+ } catch (err) {
316137
+ log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
316138
+ }
316139
+ return false;
315682
316140
  }
315683
316141
  function shouldPauseAfterTruncatedExplore(explore) {
315684
316142
  return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
@@ -315725,6 +316183,7 @@ var require_discover_agent = __commonJS({
315725
316183
  var claude_discovery_runner_js_1 = require_claude_discovery_runner();
315726
316184
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
315727
316185
  var canvas_detector_js_1 = require_canvas_detector();
316186
+ var auth_recipe_js_1 = require_auth_recipe();
315728
316187
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
315729
316188
  const cached = ctx.appRenderType ?? null;
315730
316189
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -316420,7 +316879,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316420
316879
  }
316421
316880
  if (envRefs.size > 0) {
316422
316881
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316423
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
316882
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316424
316883
  if (missing.length > 0) {
316425
316884
  autoVariableDefaults = {};
316426
316885
  for (const key of missing) {
@@ -316936,7 +317395,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316936
317395
  envRefs.add(match[1]);
316937
317396
  if (envRefs.size > 0) {
316938
317397
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316939
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
317398
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316940
317399
  if (missing.length > 0) {
316941
317400
  autoVariableDefaults = {};
316942
317401
  for (const key of missing) {
@@ -329818,9 +330277,9 @@ var require_dist2 = __commonJS({
329818
330277
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
329819
330278
  };
329820
330279
  Object.defineProperty(exports, "__esModule", { value: true });
329821
- exports.buildImportSystemRules = exports.buildImportKickoff = exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.AUTH_MARKER_VERSION = exports.buildHealAuthStamp = exports.extractAuthRegion = exports.upsertAuthBlock = exports.buildAuthRecipe = exports.commandToReplayStep = exports.buildReplayPlan = exports.absoluteRoute = exports.renderReplaySettleArrow = exports.renderReplayClickArrow = exports.renderAuthFn = exports.canonicalizeRoute = exports.executeGrokPerBatchHeal = exports.executeGrokPerTestHeal = exports.headerVal = exports.classifyAuthType = exports.isTextExcerptable = exports.isStaticAssetByExt = exports.isApiCall = exports.buildApiCallSummary = exports.computeMobileHealOutcome = exports.detectMobileStepSecretLeak = exports.redactMobileSecretsFromText = exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports.executeMobileLiveHeal = exports.executeMobileTestWithHealing = exports.executeMobileScriptDrivenHeal = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
329822
- exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = exports.MAX_MANIFEST_FILES = exports.toDiffSummary = exports.isLikelyTestFile = exports.sanitizeRepoRelPath = exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.REPO_ANALYSIS_SCHEMA_VERSION = exports.validateSourceEndpoints = exports.validateAuthoredTest = exports.validateRepoMapFile = exports.validateRepoMemoryFile = exports.buildTestingStandardsSection = exports.buildRepoAuthorSystemRules = exports.buildRepoAuthorKickoff = exports.buildRepoAuthorContractMd = exports.buildRepoMapSystemRules = exports.buildRepoMapKickoff = exports.buildRepoMapContractMd = exports.buildRepoMemorySystemRules = exports.buildRepoMemoryKickoff = exports.buildRepoMemoryContractMd = exports.runRepoAnalysisOnRunner = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = void 0;
329823
- exports.UNSUPPORTED_RENDERER_PREFIX = void 0;
330280
+ exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.isReservedRunnerEnvVar = exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = exports.buildHealAuthStamp = exports.extractAuthRegion = exports.upsertAuthBlock = exports.buildAuthRecipe = exports.commandToReplayStep = exports.buildReplayPlan = exports.absoluteRoute = exports.renderReplaySettleArrow = exports.renderReplayClickArrow = exports.renderAuthFn = exports.canonicalizeRoute = exports.executeGrokPerBatchHeal = exports.executeGrokPerTestHeal = exports.headerVal = exports.classifyAuthType = exports.isTextExcerptable = exports.isStaticAssetByExt = exports.isApiCall = exports.buildApiCallSummary = exports.computeMobileHealOutcome = exports.detectMobileStepSecretLeak = exports.redactMobileSecretsFromText = exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports.executeMobileLiveHeal = exports.executeMobileTestWithHealing = exports.executeMobileScriptDrivenHeal = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
330281
+ exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = exports.MAX_MANIFEST_FILES = exports.toDiffSummary = exports.isLikelyTestFile = exports.sanitizeRepoRelPath = exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.REPO_ANALYSIS_SCHEMA_VERSION = exports.validateSourceEndpoints = exports.validateAuthoredTest = exports.validateRepoMapFile = exports.validateRepoMemoryFile = exports.buildTestingStandardsSection = exports.buildRepoAuthorSystemRules = exports.buildRepoAuthorKickoff = exports.buildRepoAuthorContractMd = exports.buildRepoMapSystemRules = exports.buildRepoMapKickoff = exports.buildRepoMapContractMd = exports.buildRepoMemorySystemRules = exports.buildRepoMemoryKickoff = exports.buildRepoMemoryContractMd = exports.runRepoAnalysisOnRunner = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = exports.buildImportSystemRules = exports.buildImportKickoff = void 0;
330282
+ exports.UNSUPPORTED_RENDERER_PREFIX = exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = void 0;
329824
330283
  __exportStar(require_ai_provider(), exports);
329825
330284
  __exportStar(require_mobile_types(), exports);
329826
330285
  __exportStar(require_mobile_source_parser(), exports);
@@ -329957,6 +330416,12 @@ var require_dist2 = __commonJS({
329957
330416
  Object.defineProperty(exports, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
329958
330417
  return auth_recipe_js_1.AUTH_MARKER_VERSION;
329959
330418
  } });
330419
+ Object.defineProperty(exports, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
330420
+ return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
330421
+ } });
330422
+ Object.defineProperty(exports, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
330423
+ return auth_recipe_js_1.isReservedRunnerEnvVar;
330424
+ } });
329960
330425
  var auth_capture_run_js_1 = require_auth_capture_run();
329961
330426
  Object.defineProperty(exports, "runLoginCaptureRun", { enumerable: true, get: function() {
329962
330427
  return auth_capture_run_js_1.runLoginCaptureRun;
@@ -330184,7 +330649,7 @@ var require_package4 = __commonJS({
330184
330649
  "package.json"(exports, module) {
330185
330650
  module.exports = {
330186
330651
  name: "@validate.qa/runner",
330187
- version: "1.0.31",
330652
+ version: "1.0.33",
330188
330653
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
330189
330654
  bin: {
330190
330655
  "validate-runner": "dist/cli.js"
@@ -330800,25 +331265,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
330800
331265
  }
330801
331266
  return elementId;
330802
331267
  }
331268
+ async function sendKeysToFocusedInput(sessionPath, value) {
331269
+ await appiumRequest(`${sessionPath}/keys`, {
331270
+ method: "POST",
331271
+ body: JSON.stringify({ text: value, value: Array.from(value) })
331272
+ });
331273
+ }
330803
331274
  async function typeText(sessionPath, value, bounds) {
330804
331275
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330805
- if (!elementId) {
330806
- throw new Error("No focused element available for TYPE. Tap an input field first.");
331276
+ if (elementId) {
331277
+ try {
331278
+ await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
331279
+ method: "POST",
331280
+ body: JSON.stringify({ text: value })
331281
+ });
331282
+ return;
331283
+ } catch (err) {
331284
+ if (isAppiumDriverDeadError(err)) throw err;
331285
+ }
330807
331286
  }
330808
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
330809
- method: "POST",
330810
- body: JSON.stringify({ text: value })
330811
- });
331287
+ await sendKeysToFocusedInput(sessionPath, value);
330812
331288
  }
330813
331289
  async function clearField(sessionPath, bounds) {
330814
331290
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330815
- if (!elementId) {
330816
- throw new Error("No focused element available for CLEAR. Tap an input field first.");
331291
+ if (elementId) {
331292
+ try {
331293
+ await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
331294
+ method: "POST",
331295
+ body: JSON.stringify({})
331296
+ });
331297
+ return;
331298
+ } catch (err) {
331299
+ if (isAppiumDriverDeadError(err)) throw err;
331300
+ }
331301
+ }
331302
+ try {
331303
+ await appiumRequest(`${sessionPath}/keys`, {
331304
+ method: "POST",
331305
+ // Common select-all + delete sequence is platform-dependent; empty keys
331306
+ // after focus is the portable baseline Appium accepts for many hybrids.
331307
+ body: JSON.stringify({ text: "", value: [] })
331308
+ });
331309
+ } catch (err) {
331310
+ if (isAppiumDriverDeadError(err)) throw err;
331311
+ throw new Error(
331312
+ `No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
331313
+ );
330817
331314
  }
330818
- await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
330819
- method: "POST",
330820
- body: JSON.stringify({})
330821
- });
330822
331315
  }
330823
331316
  async function swipe(sessionPath, direction, distance, windowSize) {
330824
331317
  const dist = clampMobileSwipeDistance(distance);
@@ -330848,6 +331341,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
330848
331341
  async function pressBack(sessionPath) {
330849
331342
  await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
330850
331343
  }
331344
+ function isKeyboardAlreadyHiddenError(err) {
331345
+ const message = err instanceof Error ? err.message : String(err);
331346
+ return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
331347
+ }
331348
+ async function dismissKeyboard(sessionPath, platform3) {
331349
+ if (platform3 === "ANDROID") {
331350
+ await pressBack(sessionPath);
331351
+ return;
331352
+ }
331353
+ try {
331354
+ await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
331355
+ method: "POST",
331356
+ body: "{}"
331357
+ });
331358
+ return;
331359
+ } catch (err) {
331360
+ if (isAppiumDriverDeadError(err)) throw err;
331361
+ if (isKeyboardAlreadyHiddenError(err)) return;
331362
+ }
331363
+ try {
331364
+ await appiumRequest(`${sessionPath}/execute/sync`, {
331365
+ method: "POST",
331366
+ body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
331367
+ });
331368
+ return;
331369
+ } catch (err) {
331370
+ if (isAppiumDriverDeadError(err)) throw err;
331371
+ if (isKeyboardAlreadyHiddenError(err)) return;
331372
+ }
331373
+ await pressBack(sessionPath);
331374
+ }
330851
331375
  async function pressHome(sessionPath, platform3) {
330852
331376
  if (platform3 === "IOS") {
330853
331377
  await appiumRequest(`${sessionPath}/execute/sync`, {
@@ -332028,6 +332552,10 @@ async function startMobileNetworkCapture(options) {
332028
332552
 
332029
332553
  // src/services/appium-executor.ts
332030
332554
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
332555
+ var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
332556
+ function validateElementId(elementId) {
332557
+ return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
332558
+ }
332031
332559
  var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
332032
332560
  const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
332033
332561
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
@@ -332162,6 +332690,9 @@ async function createSession(target) {
332162
332690
  });
332163
332691
  const sessionId = result?.value?.sessionId ?? result?.sessionId;
332164
332692
  if (!sessionId) throw new Error("Appium session creation returned no session ID");
332693
+ if (target.platform === "ANDROID") {
332694
+ await prepareAndroidInputEnvironment(target.deviceId);
332695
+ }
332165
332696
  return sessionId;
332166
332697
  }
332167
332698
  var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
@@ -332549,7 +333080,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
332549
333080
  method: "POST",
332550
333081
  body: JSON.stringify({ using: strategy.using, value: strategy.value })
332551
333082
  });
332552
- const elementId = result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"];
333083
+ const elementId = validateElementId(
333084
+ result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
333085
+ );
332553
333086
  if (elementId) return elementId;
332554
333087
  } catch (err) {
332555
333088
  if (isTransportError(err)) throw err;
@@ -332589,24 +333122,25 @@ async function tapCoordinates(sessionId, bounds) {
332589
333122
  })
332590
333123
  });
332591
333124
  }
332592
- async function getActiveElementId2(sessionId) {
332593
- try {
332594
- const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
332595
- return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
332596
- } catch {
332597
- return null;
332598
- }
332599
- }
332600
- async function sendKeysToFocusedInput(sessionId, value) {
332601
- await wdRequest(`/session/${sessionId}/keys`, {
332602
- method: "POST",
332603
- body: JSON.stringify({ text: value, value: Array.from(value) })
332604
- });
332605
- }
332606
333125
  function encodeAndroidInputText(value) {
332607
333126
  const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
332608
333127
  return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
332609
333128
  }
333129
+ async function prepareAndroidInputEnvironment(deviceId) {
333130
+ if (!deviceId) return;
333131
+ const settingsToDisable = [
333132
+ ["secure", "stylus_handwriting_enabled", "0"]
333133
+ ];
333134
+ for (const [namespace, key, value] of settingsToDisable) {
333135
+ try {
333136
+ await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
333137
+ timeout: 8e3,
333138
+ windowsHide: true
333139
+ });
333140
+ } catch {
333141
+ }
333142
+ }
333143
+ }
332610
333144
  async function sendAndroidTextToFocusedInput(deviceId, value) {
332611
333145
  if (!deviceId) {
332612
333146
  throw new Error("Android keyboard fallback requires a device id");
@@ -332668,100 +333202,28 @@ async function executeType(sessionId, step, context) {
332668
333202
  if (step.value === void 0 || step.value === null || step.value === "") return;
332669
333203
  const value = String(step.value);
332670
333204
  const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
332671
- let elementId = null;
332672
- let lastInputError = null;
332673
- let tappedBounds = false;
332674
- if (bounds) {
332675
- await tapCoordinates(sessionId, bounds);
332676
- tappedBounds = true;
332677
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332678
- elementId = await getActiveElementId2(sessionId);
332679
- if (elementId) {
332680
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332681
- if (!lastInputError) return;
332682
- elementId = null;
332683
- }
332684
- }
332685
- if (step.target) {
332686
- try {
332687
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
332688
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332689
- if (!lastInputError) return;
332690
- elementId = null;
332691
- } catch (error2) {
332692
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
332693
- lastInputError = error2;
332694
- }
332695
- }
332696
- if (!elementId && bounds) {
332697
- await tapCoordinates(sessionId, bounds);
332698
- tappedBounds = true;
332699
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332700
- elementId = await getActiveElementId2(sessionId);
332701
- }
332702
- if (!elementId) {
332703
- elementId = await getActiveElementId2(sessionId);
332704
- }
332705
- if (elementId) {
332706
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332707
- if (!lastInputError) return;
332708
- elementId = null;
332709
- if (!bounds) {
332710
- throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
332711
- }
332712
- }
332713
- if (bounds) {
332714
- if (!tappedBounds) {
332715
- await tapCoordinates(sessionId, bounds);
332716
- tappedBounds = true;
332717
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332718
- }
332719
- try {
332720
- await sendKeysToFocusedInput(sessionId, value);
332721
- return;
332722
- } catch (error2) {
332723
- if (isTransportError(error2)) throw error2;
332724
- lastInputError = error2;
332725
- }
332726
- }
332727
- if (context.platform === "ANDROID" && tappedBounds) {
332728
- try {
332729
- context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
332730
- await sendAndroidTextToFocusedInput(context.deviceId, value);
332731
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332732
- return;
332733
- } catch (error2) {
332734
- if (isTransportError(error2)) throw error2;
332735
- lastInputError = error2;
333205
+ const sessionPath = `/session/${sessionId}`;
333206
+ try {
333207
+ await typeText(sessionPath, value, bounds);
333208
+ return;
333209
+ } catch (error2) {
333210
+ if (isTransportError(error2)) throw error2;
333211
+ if (context.platform === "ANDROID" && bounds) {
333212
+ try {
333213
+ context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
333214
+ await sendAndroidTextToFocusedInput(context.deviceId, value);
333215
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
333216
+ return;
333217
+ } catch (androidError) {
333218
+ if (isTransportError(androidError)) throw androidError;
333219
+ }
332736
333220
  }
333221
+ throw error2 instanceof Error ? error2 : new Error(String(error2));
332737
333222
  }
332738
- throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
332739
333223
  }
332740
333224
  async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
332741
333225
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
332742
- let elementId = null;
332743
- if (step.target) {
332744
- try {
332745
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
332746
- } catch (error2) {
332747
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
332748
- }
332749
- }
332750
- if (!elementId && bounds) {
332751
- await tapCoordinates(sessionId, bounds);
332752
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332753
- elementId = await getActiveElementId2(sessionId);
332754
- }
332755
- if (!elementId) {
332756
- elementId = await getActiveElementId2(sessionId);
332757
- }
332758
- if (!elementId) {
332759
- throw new Error("CLEAR step could not resolve a target or focused element");
332760
- }
332761
- await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
332762
- method: "POST",
332763
- body: "{}"
332764
- });
333226
+ await clearField(`/session/${sessionId}`, bounds);
332765
333227
  }
332766
333228
  async function executeSwipe(sessionId, step) {
332767
333229
  const direction = step.metadata?.direction || "up";
@@ -334844,6 +335306,10 @@ var MobileBridgeDriver = class {
334844
335306
  async back() {
334845
335307
  return this.runAction(() => pressBack(this.sessionPath));
334846
335308
  }
335309
+ /** Platform-aware software keyboard dismissal. */
335310
+ async dismissKeyboard() {
335311
+ return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
335312
+ }
334847
335313
  /** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
334848
335314
  async home() {
334849
335315
  return this.runAction(() => pressHome(this.sessionPath, this.platform));
@@ -335229,8 +335695,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
335229
335695
  pageUrl: call.pageUrl ?? pageUrl
335230
335696
  }));
335231
335697
  }
335698
+ var runnerPackage = require_package4();
335232
335699
  var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
335233
335700
  var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
335701
+ var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
335702
+ var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
335703
+ 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;
335234
335704
  var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
335235
335705
  var DEFAULT_TYPE_LIMITS = {
335236
335706
  discovery: IS_CLOUD ? 2 : 15,
@@ -338064,6 +338534,9 @@ async function startRunner(opts) {
338064
338534
  method: "POST",
338065
338535
  headers,
338066
338536
  body: JSON.stringify({
338537
+ runnerName: RUNNER_NAME,
338538
+ runnerVersion: RUNNER_VERSION,
338539
+ runnerBuildId: RUNNER_BUILD_ID,
338067
338540
  runsCompleted,
338068
338541
  activeRuns,
338069
338542
  maxConcurrent: MAX_CONCURRENT,