@validate.qa/runner 1.0.32 → 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 +441 -155
  2. package/dist/cli.mjs +441 -155
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -62285,7 +62285,7 @@ var require_mobile_explorer = __commonJS({
62285
62285
  return { screen, signal, refMap, observation };
62286
62286
  }
62287
62287
  function buildStatePacket(input) {
62288
- const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
62288
+ const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
62289
62289
  const sections = [];
62290
62290
  sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
62291
62291
  sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
@@ -62298,10 +62298,46 @@ var require_mobile_explorer = __commonJS({
62298
62298
  sections.push("");
62299
62299
  if (mode === "survey") {
62300
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.`);
62301
62303
  } else {
62302
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.");
62303
62305
  }
62304
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
+ }
62305
62341
  const screenCount = state2.getScreenCount();
62306
62342
  sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
62307
62343
  const currentScreenId = state2.getCurrentScreenId();
@@ -62458,7 +62494,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62458
62494
  } catch {
62459
62495
  }
62460
62496
  };
62461
- const mode = ctx.mode === "deep" ? "deep" : "survey";
62497
+ const mode = ctx.mode === "survey" ? "survey" : "deep";
62462
62498
  const platform3 = platformForMedium(ctx.medium);
62463
62499
  const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
62464
62500
  const targetAppId = ctx.target?.appId?.trim() || void 0;
@@ -62746,7 +62782,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62746
62782
  maxIterations,
62747
62783
  latestObservation: getLatest()?.observation ?? null,
62748
62784
  journeys,
62749
- transitions: collectedTransitions
62785
+ transitions: collectedTransitions,
62786
+ featureName: ctx.featureName,
62787
+ featureContext: ctx.featureContext,
62788
+ userNotes: ctx.userNotes
62750
62789
  });
62751
62790
  const messages = [
62752
62791
  { role: "system", content: systemPrompt },
@@ -66315,6 +66354,34 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
66315
66354
  if (context.featureName) {
66316
66355
  sections.push(`## Feature: ${context.featureName}
66317
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()}`);
66318
66385
  }
66319
66386
  const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66320
66387
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
@@ -66441,7 +66508,7 @@ ${constraints.join("\n")}`);
66441
66508
  routePattern: null,
66442
66509
  title: state2.screenName ?? null,
66443
66510
  pageType: state2.screenType ?? "UNKNOWN",
66444
- requiresAuth: false,
66511
+ requiresAuth: state2.screenType?.toLowerCase() === "auth",
66445
66512
  visitCount: state2.visited ? 1 : 0,
66446
66513
  lastVisitedAt: state2.visited ? now : null,
66447
66514
  elementsCovered: 0,
@@ -66461,6 +66528,8 @@ ${constraints.join("\n")}`);
66461
66528
  const toPageId = idByScreen.get(t.toScreenId);
66462
66529
  if (!fromPageId || !toPageId)
66463
66530
  continue;
66531
+ const fromScreen = graph.screens.get(t.fromScreenId);
66532
+ const toScreen = graph.screens.get(t.toScreenId);
66464
66533
  transitions.push({
66465
66534
  id: `mtx-${txSeq++}`,
66466
66535
  fromPageId,
@@ -66468,7 +66537,7 @@ ${constraints.join("\n")}`);
66468
66537
  triggerType: t.action.toUpperCase(),
66469
66538
  triggerLabel: t.viaRef ?? null,
66470
66539
  observedCount: 1,
66471
- requiresAuth: false
66540
+ requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
66472
66541
  });
66473
66542
  }
66474
66543
  return {
@@ -66671,7 +66740,9 @@ ${constraints.join("\n")}`);
66671
66740
  recordedJourneys,
66672
66741
  credentials: input.credentials,
66673
66742
  testBudget: input.testBudget,
66674
- incremental: input.incremental
66743
+ incremental: input.incremental,
66744
+ userNotes: input.userNotes,
66745
+ featureContext: input.featureContext
66675
66746
  })
66676
66747
  });
66677
66748
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
@@ -66930,8 +67001,38 @@ var require_mobile_generator = __commonJS({
66930
67001
  return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
66931
67002
  }
66932
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;
66933
67005
  function isEditableMobileElement(element) {
66934
- 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;
66935
67036
  }
66936
67037
  function nonEmpty2(value) {
66937
67038
  if (typeof value !== "string")
@@ -67423,7 +67524,12 @@ ${statePacket}` },
67423
67524
  };
67424
67525
  }
67425
67526
  const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
67426
- 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
+ }
67427
67533
  recordAction({
67428
67534
  type: "TAP",
67429
67535
  selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
@@ -67567,6 +67673,13 @@ ${statePacket}` },
67567
67673
  const element = resolveRef(toolArgs.ref);
67568
67674
  if (!element)
67569
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
+ }
67570
67683
  const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
67571
67684
  if (!expected)
67572
67685
  return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
@@ -67825,19 +67938,46 @@ ${statePacket}` },
67825
67938
  if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
67826
67939
  return "auth/session scenario tapped a login or sign-in control without recording a credential path";
67827
67940
  }
67828
- 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)
67829
67957
  return null;
67830
- const typedEmail = typedEmailIndexes.length > 0;
67831
- const typedPassword = typedPasswordIndexes.length > 0;
67832
- const tappedSubmit = submitAfterCredentials;
67833
- 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)
67834
67967
  return null;
67835
- const missing = [
67836
- typedEmail ? null : "email/username entry",
67837
- typedPassword ? null : "password entry",
67838
- tappedSubmit ? null : "sign-in submit tap"
67839
- ].filter((value) => value !== null);
67840
- 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;
67841
67981
  }
67842
67982
  function actionCycleKey(action) {
67843
67983
  return `${action.type}|${action.selector}|${action.value ?? ""}`;
@@ -68772,8 +68912,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
68772
68912
  function coerceSteps(value) {
68773
68913
  if (!Array.isArray(value) || value.length === 0)
68774
68914
  return void 0;
68775
- const allValid = value.every((step) => isRecord(step) && typeof step.action === "string" && typeof step.order === "number");
68776
- 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 }));
68777
68930
  }
68778
68931
  function parseHealVerdict(content) {
68779
68932
  const cleaned = stripCodeFences(content);
@@ -72994,6 +73147,48 @@ var require_snapshot_observation = __commonJS({
72994
73147
  var PROMOTED_FORM_FIELD_CAP = 12;
72995
73148
  var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
72996
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
+ }
72997
73192
  function isGroupedFormField(element) {
72998
73193
  if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
72999
73194
  return false;
@@ -73009,6 +73204,7 @@ var require_snapshot_observation = __commonJS({
73009
73204
  const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
73010
73205
  const artifact = page.snapshotArtifact;
73011
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");
73012
73208
  const activeScope = inferActiveSnapshotScope(page);
73013
73209
  const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
73014
73210
  const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
@@ -73022,10 +73218,17 @@ var require_snapshot_observation = __commonJS({
73022
73218
  });
73023
73219
  let interactive;
73024
73220
  if (options.mode === "survey") {
73025
- const navLinks = allInteractive.filter(({ element }) => element.elementType === "LINK" && element.triggersNavigation);
73026
- const others = allInteractive.filter(({ element }) => !(element.elementType === "LINK" && element.triggersNavigation));
73221
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73222
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
73027
73223
  const othersCap = richRender ? 6 : 4;
73028
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
+ ];
73029
73232
  } else {
73030
73233
  interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
73031
73234
  }
@@ -73038,6 +73241,36 @@ var require_snapshot_observation = __commonJS({
73038
73241
  }
73039
73242
  }
73040
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
+ }
73041
73274
  const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
73042
73275
  const allForms = artifact?.formGroups ?? page.formGroups ?? [];
73043
73276
  const forms = allForms.slice(0, options.formLimit ?? 3);
@@ -73337,6 +73570,54 @@ var require_snapshot_observation = __commonJS({
73337
73570
  const hidden = allInteractive.length - interactive.length;
73338
73571
  if (hidden > 0) {
73339
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
+ }
73340
73621
  }
73341
73622
  }
73342
73623
  }
@@ -74359,13 +74640,14 @@ var require_auth_recipe = __commonJS({
74359
74640
  "../runner-core/dist/services/auth-recipe.js"(exports2) {
74360
74641
  "use strict";
74361
74642
  Object.defineProperty(exports2, "__esModule", { value: true });
74362
- exports2.AUTH_MARKER_VERSION = void 0;
74643
+ exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
74363
74644
  exports2.computeRecipeHash = computeRecipeHash;
74364
74645
  exports2.buildAuthRecipe = buildAuthRecipe;
74365
74646
  exports2.authProofLocator = authProofLocator;
74366
74647
  exports2.urlProofPredicateExpr = urlProofPredicateExpr;
74367
74648
  exports2.renderAuthProofAssertion = renderAuthProofAssertion;
74368
74649
  exports2.renderAuthFn = renderAuthFn;
74650
+ exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
74369
74651
  exports2.absoluteRoute = absoluteRoute;
74370
74652
  exports2.renderReplayClickArrow = renderReplayClickArrow;
74371
74653
  exports2.renderReplaySettleArrow = renderReplaySettleArrow;
@@ -74484,6 +74766,17 @@ var require_auth_recipe = __commonJS({
74484
74766
  lines.push(END_MARKER);
74485
74767
  return lines.join("\n");
74486
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
+ }
74487
74780
  function absoluteRoute(route2, baseUrl) {
74488
74781
  try {
74489
74782
  return new URL(route2, baseUrl).href;
@@ -315588,9 +315881,15 @@ var require_mobile_discovery_agent = __commonJS({
315588
315881
  log2(`
315589
315882
  Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
315590
315883
  if (callbacks?.onExploreComplete) {
315591
- await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions)).catch((err) => {
315592
- log2(`[stream] explore-complete callback failed: ${err instanceof Error ? err.message : String(err)}`);
315593
- });
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
+ }
315594
315893
  }
315595
315894
  if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
315596
315895
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -315618,8 +315917,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315618
315917
  pausePhase: "explore"
315619
315918
  };
315620
315919
  }
315621
- log2("\nRelaunching app for a clean state before planning/generation...");
315622
- await driver.relaunch();
315920
+ if (!isSurvey) {
315921
+ log2("\nRelaunching app for a clean state before planning/generation...");
315922
+ await driver.relaunch();
315923
+ }
315623
315924
  log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
315624
315925
  const graph = state2.toScreenGraph();
315625
315926
  const screenMap = state2.buildInMemoryScreenMap();
@@ -315642,7 +315943,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315642
315943
  hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
315643
315944
  },
315644
315945
  incremental: ctx.incremental,
315645
- testBudget
315946
+ testBudget,
315947
+ userNotes: ctx.userNotes,
315948
+ featureContext: ctx.featureContext
315646
315949
  }, {
315647
315950
  modelOverride: ctx.planModel ?? ctx.generateModel,
315648
315951
  onAICall
@@ -315815,10 +316118,20 @@ Fatal error: ${msg}`);
315815
316118
  log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
315816
316119
  return false;
315817
316120
  }) ?? false;
315818
- if (!cleared && resetPolicy === "CLEAR_APP_DATA") {
315819
- 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;
315820
316128
  }
315821
- 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;
315822
316135
  }
315823
316136
  function shouldPauseAfterTruncatedExplore(explore) {
315824
316137
  return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
@@ -315865,6 +316178,7 @@ var require_discover_agent = __commonJS({
315865
316178
  var claude_discovery_runner_js_1 = require_claude_discovery_runner();
315866
316179
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
315867
316180
  var canvas_detector_js_1 = require_canvas_detector();
316181
+ var auth_recipe_js_1 = require_auth_recipe();
315868
316182
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
315869
316183
  const cached = ctx.appRenderType ?? null;
315870
316184
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -316560,7 +316874,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316560
316874
  }
316561
316875
  if (envRefs.size > 0) {
316562
316876
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316563
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
316877
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316564
316878
  if (missing.length > 0) {
316565
316879
  autoVariableDefaults = {};
316566
316880
  for (const key of missing) {
@@ -317076,7 +317390,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
317076
317390
  envRefs.add(match[1]);
317077
317391
  if (envRefs.size > 0) {
317078
317392
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
317079
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
317393
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
317080
317394
  if (missing.length > 0) {
317081
317395
  autoVariableDefaults = {};
317082
317396
  for (const key of missing) {
@@ -329958,9 +330272,9 @@ var require_dist2 = __commonJS({
329958
330272
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
329959
330273
  };
329960
330274
  Object.defineProperty(exports2, "__esModule", { value: true });
329961
- 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;
329962
- 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;
329963
- 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;
329964
330278
  __exportStar(require_ai_provider(), exports2);
329965
330279
  __exportStar(require_mobile_types(), exports2);
329966
330280
  __exportStar(require_mobile_source_parser(), exports2);
@@ -330097,6 +330411,12 @@ var require_dist2 = __commonJS({
330097
330411
  Object.defineProperty(exports2, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
330098
330412
  return auth_recipe_js_1.AUTH_MARKER_VERSION;
330099
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
+ } });
330100
330420
  var auth_capture_run_js_1 = require_auth_capture_run();
330101
330421
  Object.defineProperty(exports2, "runLoginCaptureRun", { enumerable: true, get: function() {
330102
330422
  return auth_capture_run_js_1.runLoginCaptureRun;
@@ -330324,7 +330644,7 @@ var require_package4 = __commonJS({
330324
330644
  "package.json"(exports2, module2) {
330325
330645
  module2.exports = {
330326
330646
  name: "@validate.qa/runner",
330327
- version: "1.0.32",
330647
+ version: "1.0.33",
330328
330648
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
330329
330649
  bin: {
330330
330650
  "validate-runner": "dist/cli.js"
@@ -330940,25 +331260,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
330940
331260
  }
330941
331261
  return elementId;
330942
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
+ }
330943
331269
  async function typeText(sessionPath, value, bounds) {
330944
331270
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330945
- if (!elementId) {
330946
- 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
+ }
330947
331281
  }
330948
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
330949
- method: "POST",
330950
- body: JSON.stringify({ text: value })
330951
- });
331282
+ await sendKeysToFocusedInput(sessionPath, value);
330952
331283
  }
330953
331284
  async function clearField(sessionPath, bounds) {
330954
331285
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330955
- if (!elementId) {
330956
- 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
+ );
330957
331309
  }
330958
- await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
330959
- method: "POST",
330960
- body: JSON.stringify({})
330961
- });
330962
331310
  }
330963
331311
  async function swipe(sessionPath, direction, distance, windowSize) {
330964
331312
  const dist = clampMobileSwipeDistance(distance);
@@ -332196,6 +332544,10 @@ async function startMobileNetworkCapture(options) {
332196
332544
 
332197
332545
  // src/services/appium-executor.ts
332198
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
+ }
332199
332551
  var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
332200
332552
  const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
332201
332553
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
@@ -332330,6 +332682,9 @@ async function createSession(target) {
332330
332682
  });
332331
332683
  const sessionId = result?.value?.sessionId ?? result?.sessionId;
332332
332684
  if (!sessionId) throw new Error("Appium session creation returned no session ID");
332685
+ if (target.platform === "ANDROID") {
332686
+ await prepareAndroidInputEnvironment(target.deviceId);
332687
+ }
332333
332688
  return sessionId;
332334
332689
  }
332335
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;
@@ -332717,7 +333072,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
332717
333072
  method: "POST",
332718
333073
  body: JSON.stringify({ using: strategy.using, value: strategy.value })
332719
333074
  });
332720
- 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
+ );
332721
333078
  if (elementId) return elementId;
332722
333079
  } catch (err) {
332723
333080
  if (isTransportError(err)) throw err;
@@ -332757,24 +333114,25 @@ async function tapCoordinates(sessionId, bounds) {
332757
333114
  })
332758
333115
  });
332759
333116
  }
332760
- async function getActiveElementId2(sessionId) {
332761
- try {
332762
- const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
332763
- return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
332764
- } catch {
332765
- return null;
332766
- }
332767
- }
332768
- async function sendKeysToFocusedInput(sessionId, value) {
332769
- await wdRequest(`/session/${sessionId}/keys`, {
332770
- method: "POST",
332771
- body: JSON.stringify({ text: value, value: Array.from(value) })
332772
- });
332773
- }
332774
333117
  function encodeAndroidInputText(value) {
332775
333118
  const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
332776
333119
  return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
332777
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
+ }
332778
333136
  async function sendAndroidTextToFocusedInput(deviceId, value) {
332779
333137
  if (!deviceId) {
332780
333138
  throw new Error("Android keyboard fallback requires a device id");
@@ -332836,100 +333194,28 @@ async function executeType(sessionId, step, context) {
332836
333194
  if (step.value === void 0 || step.value === null || step.value === "") return;
332837
333195
  const value = String(step.value);
332838
333196
  const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
332839
- let elementId = null;
332840
- let lastInputError = null;
332841
- let tappedBounds = false;
332842
- if (bounds) {
332843
- await tapCoordinates(sessionId, bounds);
332844
- tappedBounds = true;
332845
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332846
- elementId = await getActiveElementId2(sessionId);
332847
- if (elementId) {
332848
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332849
- if (!lastInputError) return;
332850
- elementId = null;
332851
- }
332852
- }
332853
- if (step.target) {
332854
- try {
332855
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
332856
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332857
- if (!lastInputError) return;
332858
- elementId = null;
332859
- } catch (error2) {
332860
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
332861
- lastInputError = error2;
332862
- }
332863
- }
332864
- if (!elementId && bounds) {
332865
- await tapCoordinates(sessionId, bounds);
332866
- tappedBounds = true;
332867
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332868
- elementId = await getActiveElementId2(sessionId);
332869
- }
332870
- if (!elementId) {
332871
- elementId = await getActiveElementId2(sessionId);
332872
- }
332873
- if (elementId) {
332874
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332875
- if (!lastInputError) return;
332876
- elementId = null;
332877
- if (!bounds) {
332878
- throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
332879
- }
332880
- }
332881
- if (bounds) {
332882
- if (!tappedBounds) {
332883
- await tapCoordinates(sessionId, bounds);
332884
- tappedBounds = true;
332885
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332886
- }
332887
- try {
332888
- await sendKeysToFocusedInput(sessionId, value);
332889
- return;
332890
- } catch (error2) {
332891
- if (isTransportError(error2)) throw error2;
332892
- lastInputError = error2;
332893
- }
332894
- }
332895
- if (context.platform === "ANDROID" && tappedBounds) {
332896
- try {
332897
- context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
332898
- await sendAndroidTextToFocusedInput(context.deviceId, value);
332899
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332900
- return;
332901
- } catch (error2) {
332902
- if (isTransportError(error2)) throw error2;
332903
- 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
+ }
332904
333212
  }
333213
+ throw error2 instanceof Error ? error2 : new Error(String(error2));
332905
333214
  }
332906
- throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
332907
333215
  }
332908
333216
  async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
332909
333217
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
332910
- let elementId = null;
332911
- if (step.target) {
332912
- try {
332913
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
332914
- } catch (error2) {
332915
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
332916
- }
332917
- }
332918
- if (!elementId && bounds) {
332919
- await tapCoordinates(sessionId, bounds);
332920
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332921
- elementId = await getActiveElementId2(sessionId);
332922
- }
332923
- if (!elementId) {
332924
- elementId = await getActiveElementId2(sessionId);
332925
- }
332926
- if (!elementId) {
332927
- throw new Error("CLEAR step could not resolve a target or focused element");
332928
- }
332929
- await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
332930
- method: "POST",
332931
- body: "{}"
332932
- });
333218
+ await clearField(`/session/${sessionId}`, bounds);
332933
333219
  }
332934
333220
  async function executeSwipe(sessionId, step) {
332935
333221
  const direction = step.metadata?.direction || "up";