@validate.qa/runner 1.0.85 → 1.0.86

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 +429 -57
  2. package/dist/cli.mjs +429 -57
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -980,10 +980,11 @@ var require_mobile_source_parser = __commonJS({
980
980
  function extractIOS(attrs) {
981
981
  const role = nonEmpty3(attrs.type) ?? "XCUIElementTypeOther";
982
982
  const name = nonEmpty3(attrs.name);
983
+ const identifier = nonEmpty3(attrs.identifier) ?? nonEmpty3(attrs.accessibilityIdentifier);
983
984
  const labelAttr = nonEmpty3(attrs.label);
984
985
  const value = nonEmpty3(attrs.value);
985
986
  const label = name ?? labelAttr ?? value;
986
- const accessibilityId = name;
987
+ const accessibilityId = identifier ?? name;
987
988
  const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
988
989
  const bounds = parseIOSBounds(attrs);
989
990
  const enabled = attrBoolDefaultTrue(attrs.enabled);
@@ -75947,6 +75948,34 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
75947
75948
  latest = boundaryResolved;
75948
75949
  lastSnapshotIteration = iterationForObservation;
75949
75950
  if (boundary.kind === "transient_external") {
75951
+ const isHostHomeShell = /springboard|systemui/i.test(boundary.owner);
75952
+ if (isHostHomeShell) {
75953
+ log2(` Host shell detected from ${source}: ${boundary.owner}; activating ${boundary.targetAppId} so exploration can continue.`);
75954
+ try {
75955
+ await driver.launchApp();
75956
+ const snap = await driver.snapshot();
75957
+ const recovered = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
75958
+ if (recovered) {
75959
+ const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
75960
+ if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
75961
+ const enriched = await enrichResolvedSnapshotWithHybridWeb(recovered, driver, credentialValuesForRedaction, log2, platform3, targetAppId, hybridContextCache, true, source !== "seed snapshot" && source !== "mobile_snapshot");
75962
+ latest = enriched;
75963
+ lastSnapshotIteration = iterationForObservation;
75964
+ const screenId = ingestSnapshot(enriched, iterationForObservation);
75965
+ captureScreenshot(enriched.replacementScreenshot ?? snap.screenshot, screenId);
75966
+ return {
75967
+ observation: enriched.observation,
75968
+ screenId,
75969
+ transientExternal: true,
75970
+ externalOwner: boundary.owner
75971
+ };
75972
+ }
75973
+ }
75974
+ } catch (err) {
75975
+ (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
75976
+ log2(` Failed to recover from host shell ${boundary.owner}: ${err instanceof Error ? err.message : String(err)}`);
75977
+ }
75978
+ }
75950
75979
  log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
75951
75980
  return {
75952
75981
  observation: `${boundaryResolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
@@ -76164,6 +76193,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76164
76193
  let summary = "";
76165
76194
  let truncated = false;
76166
76195
  let truncatedReason;
76196
+ const emptyUiTreeStreak = { count: 0 };
76167
76197
  let finishRedirects = 0;
76168
76198
  let lastFinishBreadthRemainingItems = null;
76169
76199
  let lastFinishBreadthProgressMark = null;
@@ -76502,10 +76532,17 @@ Exploration complete: ${summary}`);
76502
76532
  redactionCredentials: credentialValuesForRedaction,
76503
76533
  authTracker,
76504
76534
  inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
76505
- allowRelaunch: !preserveAppDataForTools
76535
+ allowRelaunch: !preserveAppDataForTools,
76536
+ emptyUiTreeStreak
76506
76537
  });
76507
76538
  pushResult(handled);
76508
76539
  const handledRecord = handled && typeof handled === "object" ? handled : null;
76540
+ if (handledRecord?.permanent === true && handledRecord?.emptyUiTreeStreak) {
76541
+ explorationComplete = true;
76542
+ summary = typeof handledRecord.reason === "string" && handledRecord.reason.trim() ? handledRecord.reason : "Accessibility tree remained empty after repeated Appium observations; exploration stopped early.";
76543
+ log2(`
76544
+ Stopping Phase 1 early: empty UI tree thrash (streak=${handledRecord.emptyUiTreeStreak}).`);
76545
+ }
76509
76546
  const navigationWasAttempted = handledRecord?.ok !== false || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "screenId") || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "observation");
76510
76547
  if (DEVICE_MUTATION_BARRIER_TOOLS.has(toolName) && navigationWasAttempted) {
76511
76548
  navigationBarrierTool = toolName;
@@ -76594,14 +76631,24 @@ Exploration complete: ${summary}`);
76594
76631
  }
76595
76632
  return result;
76596
76633
  }
76634
+ var MAX_EMPTY_UI_TREE_STREAK = 3;
76597
76635
  async function dispatchMobileTool(args) {
76598
- const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox, allowRelaunch } = args;
76636
+ const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox, allowRelaunch, emptyUiTreeStreak } = args;
76637
+ const noteEmptyUiTree = () => {
76638
+ emptyUiTreeStreak.count += 1;
76639
+ return emptyUiTreeStreak.count;
76640
+ };
76641
+ const clearEmptyUiTreeStreak = () => {
76642
+ emptyUiTreeStreak.count = 0;
76643
+ };
76599
76644
  const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
76600
76645
  (0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
76601
76646
  const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
76602
76647
  if (!resolved) {
76648
+ noteEmptyUiTree();
76603
76649
  return Promise.resolve({ observation: "(no UI tree returned)", screenId: null });
76604
76650
  }
76651
+ clearEmptyUiTreeStreak();
76605
76652
  return absorbResolvedObservation(resolved, actionResult.screenshot, iteration, source, preferCredentialSecondary);
76606
76653
  };
76607
76654
  const currentAuthSurface = () => {
@@ -76631,8 +76678,17 @@ Exploration complete: ${summary}`);
76631
76678
  const snap = await driver.snapshot();
76632
76679
  const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
76633
76680
  if (!resolved) {
76634
- return { ok: false, error: "snapshot returned no UI tree" };
76681
+ const streak = noteEmptyUiTree();
76682
+ log2(` Snapshot returned no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
76683
+ return {
76684
+ ok: false,
76685
+ error: "snapshot returned no UI tree",
76686
+ emptyUiTreeStreak: streak,
76687
+ permanent: streak >= MAX_EMPTY_UI_TREE_STREAK,
76688
+ reason: streak >= MAX_EMPTY_UI_TREE_STREAK ? "Appium repeatedly returned an empty accessibility tree after settle/activate. Stop relaunch thrash \u2014 call finish_exploration with evidence you have, or abort. This is almost always a session/device/app-start failure, not missing product screens." : "No accessibility tree yet. Prefer one mobile_snapshot after a short wait over repeated relaunches."
76689
+ };
76635
76690
  }
76691
+ clearEmptyUiTreeStreak();
76636
76692
  const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_snapshot");
76637
76693
  if (absorbed.screenId) {
76638
76694
  const elementCount = state2.getScreen(absorbed.screenId)?.elements.length ?? resolved.screen.elements.length;
@@ -77005,14 +77061,30 @@ Exploration complete: ${summary}`);
77005
77061
  note: "Relaunch refused under preserve policy."
77006
77062
  };
77007
77063
  }
77064
+ if (emptyUiTreeStreak.count >= MAX_EMPTY_UI_TREE_STREAK) {
77065
+ return {
77066
+ ok: false,
77067
+ error: `mobile_relaunch refused: accessibility tree has been empty for ${emptyUiTreeStreak.count} consecutive observations. Call finish_exploration or stop \u2014 further relaunches will not recover a dead session/app paint.`,
77068
+ permanent: true,
77069
+ emptyUiTreeStreak: emptyUiTreeStreak.count
77070
+ };
77071
+ }
77008
77072
  authTracker?.noteInterveningMutation(iteration);
77009
77073
  await driver.relaunch();
77010
77074
  log2(" Relaunched app for hermetic reset.");
77011
77075
  const snap = await driver.snapshot();
77012
77076
  const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
77013
77077
  if (!resolved) {
77014
- return { ok: true, note: "relaunched; no UI tree returned \u2014 call mobile_snapshot." };
77078
+ const streak = noteEmptyUiTree();
77079
+ log2(` Relaunch yielded no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
77080
+ return {
77081
+ ok: true,
77082
+ note: "relaunched; no UI tree returned \u2014 call mobile_snapshot.",
77083
+ emptyUiTreeStreak: streak,
77084
+ permanent: streak >= MAX_EMPTY_UI_TREE_STREAK
77085
+ };
77015
77086
  }
77087
+ clearEmptyUiTreeStreak();
77016
77088
  const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_relaunch");
77017
77089
  return {
77018
77090
  ok: true,
@@ -80244,6 +80316,10 @@ var require_mobile_discovery_prompts = __commonJS({
80244
80316
  "use strict";
80245
80317
  Object.defineProperty(exports, "__esModule", { value: true });
80246
80318
  exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = void 0;
80319
+ exports.isScrubbedPlaceholderName = isScrubbedPlaceholderName;
80320
+ exports.isOpaqueMobileScreenName = isOpaqueMobileScreenName;
80321
+ exports.isPackageLeafAppCatchAll = isPackageLeafAppCatchAll;
80322
+ exports.isLowValueMobileAutoDeepFeature = isLowValueMobileAutoDeepFeature;
80247
80323
  exports.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
80248
80324
  exports.transitionSourceLabel = transitionSourceLabel;
80249
80325
  exports.buildMobileSiteMap = buildMobileSiteMap;
@@ -80524,6 +80600,32 @@ Each entry is exactly one of:
80524
80600
  }
80525
80601
  return /^[a-z][\w$]*(?:\.[a-z][\w$]*)+(?:#[a-z0-9_. -]+)?$/i.test(trimmed);
80526
80602
  }
80603
+ function isPackageLeafAppCatchAll(name, appId) {
80604
+ const trimmed = name.trim();
80605
+ if (!trimmed || !appId?.trim())
80606
+ return false;
80607
+ const match = trimmed.match(/^(.+?)\s+app$/i);
80608
+ if (!match)
80609
+ return false;
80610
+ const brand = match[1].trim();
80611
+ if (!brand || /\s/.test(brand))
80612
+ return false;
80613
+ const leaf = appId.split(".").map((p) => p.trim()).filter(Boolean).pop();
80614
+ if (!leaf)
80615
+ return false;
80616
+ const normalize = (s) => s.replace(/[^a-z0-9]/gi, "").toLowerCase();
80617
+ return normalize(brand) === normalize(leaf);
80618
+ }
80619
+ function isLowValueMobileAutoDeepFeature(name, appId) {
80620
+ const trimmed = name.trim();
80621
+ if (!trimmed)
80622
+ return true;
80623
+ if (isScrubbedPlaceholderName(trimmed) || isOpaqueMobileScreenName(trimmed, appId))
80624
+ return true;
80625
+ if (isPackageLeafAppCatchAll(trimmed, appId))
80626
+ return true;
80627
+ return false;
80628
+ }
80527
80629
  function humanizeToken(token) {
80528
80630
  const spaced = token.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
80529
80631
  if (!spaced)
@@ -81555,6 +81657,7 @@ var require_mobile_generator = __commonJS({
81555
81657
  exports.coalesceRepeatedTaps = coalesceRepeatedTaps;
81556
81658
  exports.elideNoOpDetours = elideNoOpDetours;
81557
81659
  exports.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
81660
+ exports.isEphemeralContentAssertText = isEphemeralContentAssertText;
81558
81661
  exports.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
81559
81662
  exports.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
81560
81663
  exports.runMobileGeneratePhase = runMobileGeneratePhase;
@@ -82015,15 +82118,20 @@ ${hybridSkeleton.join("\n")}`;
82015
82118
  - Every element is addressed by a ref like e1, e2. Refs are ONLY valid for the most recent mobile_snapshot. Re-snapshot after every action that changes the screen, then use the NEW refs.
82016
82119
  - mobile_tap/mobile_type/mobile_clear take a ref; the loop resolves it to coordinates AND records the durable locator for replay \u2014 you never pass coordinates.
82017
82120
 
82018
- ## Your job
82121
+ ## Your job \u2014 feature fidelity (non-negotiable)
82122
+ The scenario name + goal + steps describe ONE product behavior from survey/discovery. The generated test MUST implement that behavior on THIS app \u2014 not a random walk through tabs.
82123
+
82019
82124
  1. mobile_snapshot the current screen.
82020
- 2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
82021
- 3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way. If the scenario submits a form (sign in, sign up, search, create, change password, edit profile), you MUST commit it \u2014 tap the submit/primary button OR call mobile_submit_keyboard on the last field when no button is visible. Filling fields alone is NOT the behavior under test. Navigating to a form screen without changing its fields is also NOT enough for mutation scenarios (change/create/update/edit). If it navigates, you MUST land on the destination. A "happy path" that fills a form but never commits \u2014 or only opens the form without typing \u2014 is WRONG.
82022
- 4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
82023
- - mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
82024
- - mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
82025
- - Assert the REAL post-action outcome: after a sign-in, assert a signed-in signal (dashboard/feed content, account tab, "Sign out"); after an invalid submit, assert the error message; after navigation, assert an element unique to the destination screen. Do NOT assert an opaque internal id ("step-1", "step-2") as the outcome \u2014 pick a human-meaningful element.
82026
- 5. Call finish_generate with verified:true ONLY if you asserted the outcome with an assert tool.
82125
+ 2. Execute the scenario's steps using the real UI: tap the controls that perform the goal, fill fields (mobile_type), submit, navigate to the destination named in the goal.
82126
+ 3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way:
82127
+ - Form scenarios (sign in, sign up, search, create, change password, edit profile): you MUST commit \u2014 tap submit/primary OR mobile_submit_keyboard. Filling alone is NOT enough. Opening a form without typing is NOT enough for mutation goals.
82128
+ - Content actions (like, share, follow, bookmark, download, \u2026): you MUST tap the control that performs that action \u2014 tab thrash / swipes alone are WRONG.
82129
+ - Navigation scenarios: you MUST land on the destination and assert something unique to it.
82130
+ 4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
82131
+ - mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
82132
+ - After sign-in \u2192 signed-in product signal; after like \u2192 liked/unlike control or durable like state; after open Settings \u2192 a settings-specific control.
82133
+ - Do NOT assert bottom-nav tab labels alone, notification badge counts ("N unread"), or one-time empty-state CTAs as the scenario outcome.
82134
+ 5. Call finish_generate with verified:true ONLY if you completed the goal AND asserted a matching durable outcome. If the app cannot complete the goal, finish with verified:false and explain \u2014 never invent a green path from unrelated screens.
82027
82135
 
82028
82136
  ## Rules
82029
82137
  - Re-snapshot before every assertion so the ref is fresh.
@@ -82074,7 +82182,7 @@ ${stepLines || " (no explicit steps \u2014 explore the goal directly)"}
82074
82182
  Expected outcomes to PROVE with assert tools:
82075
82183
  ${outcomeLines || " - The target screen is reached and a stable element is visible."}
82076
82184
 
82077
- Start by calling mobile_snapshot, then execute the steps above \u2014 the minimum actions needed to reach the goal, without repeating, reverting, or exploring beyond them. Finish with at least one mobile_assert_visible / mobile_assert_text proving the outcome, then finish_generate.`;
82185
+ Start by calling mobile_snapshot, then execute ONLY the steps needed for this goal \u2014 no tab thrash, no unrelated screens. Every recorded action becomes a permanent test step. Finish with at least one assert that proves THIS goal's outcome (from Expected outcomes), then finish_generate. If you cannot perform the goal on this app build, finish_generate with verified:false rather than a fake green path.`;
82078
82186
  }
82079
82187
  function buildStatePacket(input) {
82080
82188
  const { scenario, iteration, maxIterations, latestObservation, assertCount, actionCount } = input;
@@ -83541,6 +83649,84 @@ ${statePacket}`, redactionCredentials) },
83541
83649
  "element-hidden",
83542
83650
  "screen-visible"
83543
83651
  ]);
83652
+ var CONTENT_ACTION_VERB_RE = /\b(like|unlike|favorite|favourite|unfavorite|unfavourite|heart|unheart|follow|unfollow|share|repost|retweet|bookmark|unbookmark|save\s+to|download|subscribe|unsubscribe|block|report|unpin|pin\s+(?:post|item|message|tweet)|mute|unmute|react|upvote|downvote|clap|boost)\b/i;
83653
+ var NAV_CHROME_TAP_RE = /\b(home|you|profile|search|settings|notifications?|inbox|messages?|explore|library|more|menu|tab|bottom\s*nav|videos?|create|feed)\b/i;
83654
+ var VOLATILE_BADGE_ASSERT_RE = /\b\d+\s*(unread|notifications?|messages?|likes?|followers?|comments?|views?)\b|\bunread\b.*\b\d+\b/i;
83655
+ function isContentActionScenario(scenario) {
83656
+ const text = scenarioText(scenario);
83657
+ if (!CONTENT_ACTION_VERB_RE.test(text))
83658
+ return false;
83659
+ if (/\b(view|see|show|open|browse|list|count)\s+(the\s+)?(likes?|followers?|shares?|bookmarks?)\b/i.test(text)) {
83660
+ return false;
83661
+ }
83662
+ return true;
83663
+ }
83664
+ function isContentActionTap(action) {
83665
+ if (action.type !== "TAP")
83666
+ return false;
83667
+ const textForAction = actionText(action);
83668
+ if (NAV_CHROME_TAP_RE.test(textForAction) && !CONTENT_ACTION_VERB_RE.test(textForAction)) {
83669
+ return false;
83670
+ }
83671
+ return CONTENT_ACTION_VERB_RE.test(textForAction);
83672
+ }
83673
+ function isProductiveScenarioAction(action, scenarioTextLower = "") {
83674
+ if (action.type === "TYPE" || action.type === "CLEAR")
83675
+ return true;
83676
+ if (action.type === "TAP") {
83677
+ const t = actionText(action);
83678
+ if (CONTENT_ACTION_VERB_RE.test(t) || isFormCommitAction(action) || isCredentialSubmitTap(action)) {
83679
+ return true;
83680
+ }
83681
+ if (NAV_CHROME_TAP_RE.test(t)) {
83682
+ if (!scenarioTextLower)
83683
+ return false;
83684
+ const chromeWords = t.match(/\b(home|you|profile|search|settings|notifications?|inbox|messages?|explore|library|more|menu|videos?|create|feed)\b/gi) ?? [];
83685
+ return chromeWords.some((w) => scenarioTextLower.includes(w.toLowerCase()));
83686
+ }
83687
+ return true;
83688
+ }
83689
+ return false;
83690
+ }
83691
+ function isNavChromeThrashTrace(actions) {
83692
+ const meaningful = actions.filter((a) => a.type !== "WAIT_FOR_ELEMENT");
83693
+ if (meaningful.length === 0)
83694
+ return false;
83695
+ const hasNavOrGesture = meaningful.some((a) => a.type === "TAP" || a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME");
83696
+ if (!hasNavOrGesture)
83697
+ return false;
83698
+ return meaningful.every((a) => {
83699
+ if (a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME")
83700
+ return true;
83701
+ if (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") {
83702
+ const t = actionText(a);
83703
+ return NAV_CHROME_TAP_RE.test(t) || VOLATILE_BADGE_ASSERT_RE.test(t) || isStructuralOnlyAssert(a);
83704
+ }
83705
+ if (a.type === "TAP") {
83706
+ return NAV_CHROME_TAP_RE.test(actionText(a)) && !CONTENT_ACTION_VERB_RE.test(actionText(a));
83707
+ }
83708
+ return false;
83709
+ });
83710
+ }
83711
+ function isHarnessOrVisibilityScenario(scenario) {
83712
+ const text = scenarioText(scenario);
83713
+ return /\b(avoid|recover|guard|remain|stay\s+in|does\s+not\s+record|external|outside\s+the\s+app|inspect|visibility\s+proof|landing\s+screen|smoke|structural\s+assert|skip\s+link|global\s+navigation\s+anchors?)\b/i.test(text);
83714
+ }
83715
+ function requiresFeatureProgress(scenario) {
83716
+ if (isNavigationFocusedScenario(scenario))
83717
+ return false;
83718
+ if (isCancelDiscardScenario(scenario))
83719
+ return false;
83720
+ if (isContentActionScenario(scenario))
83721
+ return true;
83722
+ if (isHarnessOrVisibilityScenario(scenario))
83723
+ return false;
83724
+ const text = scenarioText(scenario);
83725
+ const behaviorClaim = scenario.type === "HAPPY_PATH" || scenario.type === "USER_JOURNEY" || /\b(happy\s*path|successful|success|complete|user\s+can|should\s+be\s+able)\b/i.test(text);
83726
+ if (!behaviorClaim)
83727
+ return false;
83728
+ return /\b(like|unlike|share|follow|bookmark|download|subscribe|create|save|send|submit|edit|delete|remove|add|play|search|type|fill|login|sign\s*in|log\s*in|register|checkout|update|change|open|view|browse|filter|sort|upload|invite|join)\b/i.test(text);
83729
+ }
83544
83730
  function isInspectionOnlyScenario(scenario, actions) {
83545
83731
  if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
83546
83732
  return false;
@@ -83550,16 +83736,25 @@ ${statePacket}`, redactionCredentials) },
83550
83736
  if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
83551
83737
  return false;
83552
83738
  }
83739
+ if (isContentActionScenario(scenario)) {
83740
+ return false;
83741
+ }
83553
83742
  const text = scenarioText(scenario);
83554
- const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|options?|provider\s+buttons?)\b/.test(text) || /\b(show|display)\s+(all|the)?\s*(provider|option|button|link|entry)/.test(text) || /\b(all\s+(three\s+)?providers?)\b/.test(text)) && !/\b(change|update|create|save|send|register|edit|with\s+valid|successful\s+(sign|log)|complete\s+(the\s+)?(login|sign[- ]?in)|submit\s+(the\s+)?(form|credentials)|enter\s+(an?\s+)?(email|password))\b/.test(text);
83743
+ const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|provider\s+buttons?|sign[- ]?in\s+options|auth(?:entication)?\s+options|available\s+options)\b/.test(text) || /\b(show|display)\s+(all\s+)?(the\s+)?(providers?|options|buttons?|links?|entry\s+points?)\b/.test(text) || /\b(all\s+(three\s+)?providers?)\b/.test(text)) && !/\b(change|update|create|save|send|register|edit|with\s+valid|successful\s+(sign|log)|complete\s+(the\s+)?(login|sign[- ]?in)|submit\s+(the\s+)?(form|credentials)|enter\s+(an?\s+)?(email|password))\b/.test(text);
83555
83744
  if (visibilityOnlyGoal) {
83556
83745
  const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83557
83746
  return assertCount2 > 0;
83558
83747
  }
83559
- const completionIntent = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase))\b/.test(text);
83748
+ const completionIntent = scenario.type === "HAPPY_PATH" || scenario.type === "USER_JOURNEY" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase)|user\s+can|should\s+be\s+able)\b/.test(text);
83560
83749
  const formIntent = /\b(sign\s*in|log\s*in|login|authenticate|register|sign\s*up|checkout|create|save|submit|send|change|update|edit)\b/.test(text);
83561
83750
  if (completionIntent && formIntent)
83562
83751
  return false;
83752
+ if (completionIntent && /\b(like|unlike|share|follow|bookmark|download|subscribe|create|save|send|post|delete|remove|add|play|pause)\b/i.test(text)) {
83753
+ return false;
83754
+ }
83755
+ if (completionIntent) {
83756
+ return false;
83757
+ }
83563
83758
  const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83564
83759
  return assertCount > 0;
83565
83760
  }
@@ -83569,7 +83764,18 @@ ${statePacket}`, redactionCredentials) },
83569
83764
  const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
83570
83765
  return navigates && !completesFlow;
83571
83766
  }
83767
+ function isCancelDiscardScenario(scenario) {
83768
+ const text = scenarioText(scenario);
83769
+ if (/\bcancel\s+(?:the|my|a|an|this|your|their)?\s*(subscription|order|booking|trip|reservation|membership|plan|purchase|payment|appointment|ticket|flight|ride)\b/i.test(text)) {
83770
+ return false;
83771
+ }
83772
+ const cancelEditPhrase = /\b(cancel|discard|revert|undo)\s+(?:(?:the|my|a|an|this|your|their|all)\s+)?(?:unsaved\s+)?(change|changes|edit|edits|update|updates|draft|input|inputs)\b/i.test(text) || /\b(change|changes|edit|edits|update|updates|draft)\s+(?:are\s+)?(cancelled|canceled|discarded|reverted|undone)\b/i.test(text) || /\breject\s+(?:(?:the|my|a|an|this|your|their|all)\s+)?(change|changes|edit|edits|update|updates|draft)\b/i.test(text);
83773
+ const discardWithoutSave = /\b(without\s+saving|do\s+not\s+save|don't\s+save)\b/i.test(text) && /\b(cancel|discard|revert|undo|leave|exit|back|dismiss)\b/i.test(text);
83774
+ return cancelEditPhrase || discardWithoutSave;
83775
+ }
83572
83776
  function isFormValidationScenario(scenario) {
83777
+ if (isCancelDiscardScenario(scenario))
83778
+ return false;
83573
83779
  if (scenario.variant === "validation")
83574
83780
  return true;
83575
83781
  if (scenario.type !== "EDGE_CASE")
@@ -83580,6 +83786,8 @@ ${statePacket}`, redactionCredentials) },
83580
83786
  function isFormMutationHappyPath(scenario) {
83581
83787
  if (isNavigationFocusedScenario(scenario))
83582
83788
  return false;
83789
+ if (isCancelDiscardScenario(scenario))
83790
+ return false;
83583
83791
  if (isFormValidationScenario(scenario))
83584
83792
  return false;
83585
83793
  const text = scenarioText(scenario);
@@ -83592,9 +83800,14 @@ ${statePacket}`, redactionCredentials) },
83592
83800
  if (/\b(show|display|visible|options?|providers?|links?)\b/.test(text) && !/\b(change|update|create|save|send|register|edit|post|write|compose|delete|add|set)\b/.test(text)) {
83593
83801
  return false;
83594
83802
  }
83595
- const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
83596
- const hasObject = /\b(password|email|profile|message|post|item|record|form|settings|account|name|title|description|comment|bio|phone|address|payment|card|user|note|task|entry)\b/.test(text);
83597
- return mutates && hasObject;
83803
+ if (/\b(play|pause|resume|playback|seek|volume|mute|unmute|shuffle|repeat|now\s+playing|mini\s*player)\b/.test(text) && !/\b(playlist\s+name|edit\s+(track|song|clip)|upload|caption|comment|description|save\s+to|add\s+to\s+(a\s+)?playlist|create\s+(a\s+)?playlist|rename)\b/.test(text)) {
83804
+ return false;
83805
+ }
83806
+ const formObject = "password|email|profile|message|post|item|record|form|settings|account|name|title|description|comment|bio|phone|address|payment|card|user|note|task|entry";
83807
+ const mutationVerb = "change|update|create|save|send|register|sign\\s*up|checkout|edit|post|write|compose|set|add|delete|remove";
83808
+ const determiner = "(?:(?:the|a|an|my|your|their|this|that)\\s+)?(?:new\\s+)?";
83809
+ const mutatesFormObject = new RegExp(`\\b(?:${mutationVerb})\\s+${determiner}(?:${formObject})\\b`).test(text) || new RegExp(`\\b(?:${formObject})\\s+(?:${mutationVerb})\\b`).test(text);
83810
+ return mutatesFormObject;
83598
83811
  }
83599
83812
  function isNavigationChromeTap(action) {
83600
83813
  if (action.type !== "TAP")
@@ -83683,12 +83896,46 @@ ${statePacket}`, redactionCredentials) },
83683
83896
  }
83684
83897
  function scenarioIntentMismatchReason(scenario, actions) {
83685
83898
  const text = scenarioText(scenario);
83899
+ if (isContentActionScenario(scenario)) {
83900
+ const hasActionTap = actions.some((a) => isContentActionTap(a));
83901
+ if (!hasActionTap) {
83902
+ return "content-action scenario (like/share/follow/\u2026) did not record a tap on a matching control";
83903
+ }
83904
+ if (isNavChromeThrashTrace(actions)) {
83905
+ return "content-action scenario only recorded tab thrash / swipes \u2014 no product action";
83906
+ }
83907
+ const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
83908
+ if (asserts.length > 0 && asserts.every((a) => VOLATILE_BADGE_ASSERT_RE.test(actionText(a)) || NAV_CHROME_TAP_RE.test(actionText(a)) && !CONTENT_ACTION_VERB_RE.test(actionText(a)))) {
83909
+ return "content-action scenario only asserted tab chrome or a volatile badge count \u2014 assert the action outcome instead";
83910
+ }
83911
+ return null;
83912
+ }
83686
83913
  if (isInspectionOnlyScenario(scenario, actions)) {
83687
83914
  return null;
83688
83915
  }
83689
83916
  if (isNavigationFocusedScenario(scenario)) {
83690
83917
  return null;
83691
83918
  }
83919
+ if (isCancelDiscardScenario(scenario)) {
83920
+ const hasCancelTap = actions.some((a) => {
83921
+ if (a.type !== "TAP")
83922
+ return false;
83923
+ return /\b(cancel|discard|close|dismiss|revert|undo|not\s+now)\b/.test(actionText(a));
83924
+ });
83925
+ const lastNavIdx = (() => {
83926
+ for (let i = actions.length - 1; i >= 0; i--) {
83927
+ const t = actions[i].type;
83928
+ if (t === "TAP" || t === "BACK" || t === "TYPE" || t === "CLEAR")
83929
+ return i;
83930
+ }
83931
+ return -1;
83932
+ })();
83933
+ const backAsDismiss = lastNavIdx >= 0 && actions[lastNavIdx].type === "BACK" && /\b(back|leave|exit|dismiss)\b/i.test(text) && /\b(without\s+saving|do\s+not\s+save|don't\s+save|discard|cancel)\b/i.test(text);
83934
+ if (!hasCancelTap && !backAsDismiss) {
83935
+ return "cancel/discard scenario did not record a cancel/dismiss action";
83936
+ }
83937
+ return null;
83938
+ }
83692
83939
  if (isFormValidationScenario(scenario)) {
83693
83940
  const typedFields = actions.some((a) => a.type === "TYPE");
83694
83941
  const hasCommit = actions.some((a) => isFormCommitAction(a));
@@ -83782,30 +84029,42 @@ ${statePacket}`, redactionCredentials) },
83782
84029
  }
83783
84030
  }
83784
84031
  const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
83785
- if (!isHappyPath)
83786
- return null;
83787
- let lastTypeIndex = -1;
83788
- let lastAssertIndex = -1;
83789
- actions.forEach((action, index) => {
83790
- if (action.type === "TYPE")
83791
- lastTypeIndex = index;
83792
- if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
83793
- lastAssertIndex = index;
83794
- });
83795
- if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
83796
- return null;
83797
- const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
83798
- const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
83799
- const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
83800
- const lastTypeAction = actions[lastTypeIndex];
83801
- const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
83802
- const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
83803
- if (index <= lastTypeIndex || index >= lastAssertIndex)
83804
- return false;
83805
- return isFormCommitAction(action);
83806
- });
83807
- if (screensDiffer && !commitAfterType) {
83808
- return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
84032
+ if (isHappyPath) {
84033
+ let lastTypeIndex = -1;
84034
+ let lastAssertIndex = -1;
84035
+ actions.forEach((action, index) => {
84036
+ if (action.type === "TYPE")
84037
+ lastTypeIndex = index;
84038
+ if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
84039
+ lastAssertIndex = index;
84040
+ });
84041
+ if (lastTypeIndex >= 0 && lastAssertIndex > lastTypeIndex) {
84042
+ const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
84043
+ const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
84044
+ const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
84045
+ const lastTypeAction = actions[lastTypeIndex];
84046
+ const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
84047
+ const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
84048
+ if (index <= lastTypeIndex || index >= lastAssertIndex)
84049
+ return false;
84050
+ return isFormCommitAction(action);
84051
+ });
84052
+ if (screensDiffer && !commitAfterType) {
84053
+ return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
84054
+ }
84055
+ }
84056
+ }
84057
+ if (isNavChromeThrashTrace(actions) && !isNavigationFocusedScenario(scenario) && !isFormMutationHappyPath(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario && !isHarnessOrVisibilityScenario(scenario)) {
84058
+ return "recording is only bottom-nav thrash and swipes \u2014 does not execute the scenario goal";
84059
+ }
84060
+ if (requiresFeatureProgress(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario) {
84061
+ if (!actions.some((a) => isProductiveScenarioAction(a, text))) {
84062
+ return "happy-path / user-journey scenario did not record a productive product action (only chrome, gestures, or empty work)";
84063
+ }
84064
+ const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
84065
+ if (asserts.length > 0 && asserts.every((a) => VOLATILE_BADGE_ASSERT_RE.test(actionText(a)) || isStructuralOnlyAssert(a))) {
84066
+ return "happy-path / user-journey scenario only asserted volatile badges or structural roots \u2014 assert a durable feature outcome";
84067
+ }
83809
84068
  }
83810
84069
  return null;
83811
84070
  }
@@ -84060,6 +84319,29 @@ ${statePacket}`, redactionCredentials) },
84060
84319
  lines.push("});");
84061
84320
  return lines.join("\n");
84062
84321
  }
84322
+ function isEphemeralContentAssertText(text) {
84323
+ const t = text.trim();
84324
+ if (!t)
84325
+ return false;
84326
+ if (/\b\d+\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|mo|mos|month|months|y|yr|yrs|year|years)\s+ago\b/i.test(t)) {
84327
+ return true;
84328
+ }
84329
+ if (/\b(just\s+now|moments?\s+ago|yesterday)\b/i.test(t))
84330
+ return true;
84331
+ if (/\btoday\s+at\s+\d{1,2}:\d{2}\b/i.test(t))
84332
+ return true;
84333
+ const feedish = /\b(followed|liked|commented|replied|mentioned|shared|posted|unread|notification|message from|new follower)\b/i.test(t) || /\b\d+\s*(s|m|h|d|w)\s+ago\b/i.test(t);
84334
+ if (feedish && t.length >= 70 && /[.!?].+\s/.test(t) && t.split(/\s+/).length >= 10) {
84335
+ return true;
84336
+ }
84337
+ if (t.length >= 40 && /\b(error\s+\d{3,}|generation failed|rate limit|quota exceeded)\b/i.test(t)) {
84338
+ return true;
84339
+ }
84340
+ return false;
84341
+ }
84342
+ function assertIdentityText(action) {
84343
+ return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
84344
+ }
84063
84345
  function evaluateMobileAssertionStrength(actions) {
84064
84346
  const modelAsserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
84065
84347
  if (modelAsserts.length === 0) {
@@ -84095,6 +84377,24 @@ ${statePacket}`, redactionCredentials) },
84095
84377
  }
84096
84378
  };
84097
84379
  }
84380
+ const ephemeral = modelAsserts.filter((a) => isEphemeralContentAssertText(assertIdentityText(a)));
84381
+ const durable = modelAsserts.filter((a) => !isEphemeralContentAssertText(assertIdentityText(a)));
84382
+ if (ephemeral.length > 0 && durable.length === 0) {
84383
+ return {
84384
+ ok: false,
84385
+ severity: "block",
84386
+ reason: "only ephemeral content assertions (relative times, long feed/notification copy, or backend error strings) \u2014 assert durable chrome or run-scoped data instead",
84387
+ signals: {
84388
+ apiTest: false,
84389
+ interacted: true,
84390
+ mutated: actions.some((a) => a.type === "TYPE"),
84391
+ navOnly: false,
84392
+ hasOutcomeProof: false,
84393
+ errorBanner: false,
84394
+ terminal: null
84395
+ }
84396
+ };
84397
+ }
84098
84398
  const code = mobileActionsToPseudoPlaywright(actions);
84099
84399
  const interacted = actions.some((a) => a.type === "TAP" || a.type === "TYPE" || a.type === "CLEAR");
84100
84400
  return (0, shared_1.validateAssertionStrength)(code, { interactionPerformed: interacted });
@@ -84970,6 +85270,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
84970
85270
  For a fix, return the COMPLETE steps array (not a diff).`;
84971
85271
  var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
84972
85272
  var RECOVERABLE_MAX = 3;
85273
+ var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
84973
85274
  var BONUS_ATTEMPTS_CEIL = 4;
84974
85275
  var DEFAULT_BONUS_ATTEMPTS = 2;
84975
85276
  var MAX_LOG_CHARS = 4096;
@@ -85575,6 +85876,7 @@ ${args.directive}`);
85575
85876
  const triedLocators = [];
85576
85877
  let sameErrorCount = 0;
85577
85878
  let lastErrorSignature = null;
85879
+ let consecutiveInvalidVerdicts = 0;
85578
85880
  const markRecoverableTurn = () => {
85579
85881
  if (recoverableUsed < RECOVERABLE_MAX) {
85580
85882
  attemptCap += 1;
@@ -85648,11 +85950,22 @@ ${args.directive}`);
85648
85950
  aiResponseCount += 1;
85649
85951
  const parsed = parseHealVerdict(content);
85650
85952
  if (!parsed) {
85651
- options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict (${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 granting a recovery turn`);
85953
+ consecutiveInvalidVerdicts += 1;
85954
+ options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict (${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 granting a recovery turn (${consecutiveInvalidVerdicts}/${MAX_CONSECUTIVE_INVALID_VERDICTS} consecutive)`);
85955
+ if (consecutiveInvalidVerdicts >= MAX_CONSECUTIVE_INVALID_VERDICTS) {
85956
+ options.onProgress?.(`Phase 1 aborted: ${consecutiveInvalidVerdicts} consecutive invalid verdicts \u2014 freeing device`);
85957
+ return {
85958
+ passed: false,
85959
+ verdict: "config_issue",
85960
+ healReason: `heal model returned ${consecutiveInvalidVerdicts} consecutive non-verdict responses (last ${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 aborting to free the device`,
85961
+ attemptsUsed: attempt
85962
+ };
85963
+ }
85652
85964
  markRecoverableTurn();
85653
- pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object.";
85965
+ pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object \u2014 no prose, no markdown fences.";
85654
85966
  continue;
85655
85967
  }
85968
+ consecutiveInvalidVerdicts = 0;
85656
85969
  if (TERMINAL_VERDICTS.has(parsed.verdict)) {
85657
85970
  return { passed: false, verdict: parsed.verdict, healReason: parsed.reason, attemptsUsed: attempt };
85658
85971
  }
@@ -353039,9 +353352,9 @@ var require_dist2 = __commonJS({
353039
353352
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
353040
353353
  };
353041
353354
  Object.defineProperty(exports, "__esModule", { value: true });
353042
- exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.filterLoginCaptureCredentials = exports.buildLoginCaptureCredentials = 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.filterFeatureGroupsToPathScope = exports.isAuthFlowRoute = exports.deriveSurveyPathScope = exports.canonicalizeRoute = exports.codeUsesInboxHelper = exports.codeUsesTestInboxIdentity = exports.codeSelfProvisionsRegistration = 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.prepareMobileScenarioAppState = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
353043
- 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.validateAuthoredMobileTest = exports.buildMobileRepoAuthorSystemRules = exports.buildMobileRepoAuthorKickoff = exports.buildMobileRepoAuthorContractMd = 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.MOBILE_IMPORT_SCHEMA_VERSION = exports.validateImportedMobileTest = exports.buildMobileImportSystemRules = exports.buildMobileImportKickoff = exports.buildMobileImportContractMd = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = exports.buildImportSystemRules = exports.buildImportKickoff = exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = void 0;
353044
- exports.BrokeredInboxReader = exports.applyPlusTag = exports.deriveProjectInboxTag = exports.deriveInboxTag = exports.loadVerificationInboxConfig = exports.VerificationInboxReader = exports.UNSUPPORTED_RENDERER_PREFIX = exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.truncateAtLine = exports.neutralizeUntrustedBlock = exports.renderUntrustedKnowledgeBlock = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = void 0;
353355
+ exports.filterLoginCaptureCredentials = exports.buildLoginCaptureCredentials = 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.filterFeatureGroupsToPathScope = exports.isAuthFlowRoute = exports.deriveSurveyPathScope = exports.canonicalizeRoute = exports.codeUsesInboxHelper = exports.codeUsesTestInboxIdentity = exports.codeSelfProvisionsRegistration = 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.prepareMobileScenarioAppState = exports.runMobileGeneratePhase = exports.isLowValueMobileAutoDeepFeature = exports.isPackageLeafAppCatchAll = exports.isOpaqueMobileScreenName = exports.isScrubbedPlaceholderName = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
353356
+ exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.validateAuthoredMobileTest = exports.buildMobileRepoAuthorSystemRules = exports.buildMobileRepoAuthorKickoff = exports.buildMobileRepoAuthorContractMd = 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.MOBILE_IMPORT_SCHEMA_VERSION = exports.validateImportedMobileTest = exports.buildMobileImportSystemRules = exports.buildMobileImportKickoff = exports.buildMobileImportContractMd = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = 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 = void 0;
353357
+ exports.BrokeredInboxReader = exports.applyPlusTag = exports.deriveProjectInboxTag = exports.deriveInboxTag = exports.loadVerificationInboxConfig = exports.VerificationInboxReader = exports.UNSUPPORTED_RENDERER_PREFIX = exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.truncateAtLine = exports.neutralizeUntrustedBlock = exports.renderUntrustedKnowledgeBlock = 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 = void 0;
353045
353358
  __exportStar(require_ai_provider(), exports);
353046
353359
  __exportStar(require_mobile_types(), exports);
353047
353360
  __exportStar(require_mobile_source_parser(), exports);
@@ -353066,6 +353379,18 @@ var require_dist2 = __commonJS({
353066
353379
  Object.defineProperty(exports, "runMobileAIPlanner", { enumerable: true, get: function() {
353067
353380
  return mobile_discovery_prompts_js_1.runMobileAIPlanner;
353068
353381
  } });
353382
+ Object.defineProperty(exports, "isScrubbedPlaceholderName", { enumerable: true, get: function() {
353383
+ return mobile_discovery_prompts_js_1.isScrubbedPlaceholderName;
353384
+ } });
353385
+ Object.defineProperty(exports, "isOpaqueMobileScreenName", { enumerable: true, get: function() {
353386
+ return mobile_discovery_prompts_js_1.isOpaqueMobileScreenName;
353387
+ } });
353388
+ Object.defineProperty(exports, "isPackageLeafAppCatchAll", { enumerable: true, get: function() {
353389
+ return mobile_discovery_prompts_js_1.isPackageLeafAppCatchAll;
353390
+ } });
353391
+ Object.defineProperty(exports, "isLowValueMobileAutoDeepFeature", { enumerable: true, get: function() {
353392
+ return mobile_discovery_prompts_js_1.isLowValueMobileAutoDeepFeature;
353393
+ } });
353069
353394
  var mobile_generator_js_1 = require_mobile_generator();
353070
353395
  Object.defineProperty(exports, "runMobileGeneratePhase", { enumerable: true, get: function() {
353071
353396
  return mobile_generator_js_1.runMobileGeneratePhase;
@@ -353501,7 +353826,7 @@ var require_package4 = __commonJS({
353501
353826
  "package.json"(exports, module) {
353502
353827
  module.exports = {
353503
353828
  name: "@validate.qa/runner",
353504
- version: "1.0.85",
353829
+ version: "1.0.86",
353505
353830
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353506
353831
  bin: {
353507
353832
  "validate-runner": "dist/cli.js"
@@ -355413,7 +355738,7 @@ async function pressHome(sessionPath, platform3) {
355413
355738
  // src/services/device-quarantine.ts
355414
355739
  var QUARANTINE_MAX_MS = (() => {
355415
355740
  const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
355416
- return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
355741
+ return Number.isFinite(raw) && raw > 0 ? raw : 2 * 60 * 1e3;
355417
355742
  })();
355418
355743
  var quarantinedAt = /* @__PURE__ */ new Map();
355419
355744
  var nextQuarantineToken = 1;
@@ -356913,6 +357238,12 @@ async function createSession(target) {
356913
357238
  // Scoped to simulator UDIDs (a physical iOS device has no such step);
356914
357239
  // override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
356915
357240
  ...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
357241
+ // WDA cold-start is flaky under dual-sim load (xcodebuild 65/75, SIGTERM).
357242
+ // Retry startup instead of failing the whole discovery session once.
357243
+ "appium:wdaStartupRetries": 4,
357244
+ "appium:wdaStartupRetryInterval": 2e3,
357245
+ "appium:wdaLaunchTimeout": 12e4,
357246
+ "appium:wdaConnectionTimeout": 12e4,
356916
357247
  ...parallelCaps,
356917
357248
  ...realCaps
356918
357249
  } : {
@@ -356924,6 +357255,12 @@ async function createSession(target) {
356924
357255
  "appium:noReset": true,
356925
357256
  // See the iOS branch: the 60s default kills AI-driven discovery sessions.
356926
357257
  "appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
357258
+ // Default UIA2 server launch is 30s — under dual-emu / post-quarantine
357259
+ // stress instrumentation often needs longer or the session never binds
357260
+ // ("instrumentation process cannot be initialized within 30000ms").
357261
+ "appium:uiautomator2ServerLaunchTimeout": 12e4,
357262
+ "appium:uiautomator2ServerInstallTimeout": 12e4,
357263
+ "appium:adbExecTimeout": 12e4,
356927
357264
  "appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
356928
357265
  ...parallelCaps,
356929
357266
  ...realCaps
@@ -359702,6 +360039,12 @@ async function startSession(validated, res) {
359702
360039
  // overrides. Verified live: without this, recorder/mirror sessions on iOS
359703
360040
  // die with "Simulator.app does not exist" while executor sessions work.
359704
360041
  ...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
360042
+ // Match executor createSession: tolerate WDA cold-start flakes.
360043
+ "appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
360044
+ "appium:wdaStartupRetries": 4,
360045
+ "appium:wdaStartupRetryInterval": 2e3,
360046
+ "appium:wdaLaunchTimeout": 12e4,
360047
+ "appium:wdaConnectionTimeout": 12e4,
359705
360048
  ...realCaps
359706
360049
  } : {
359707
360050
  platformName: "Android",
@@ -359710,6 +360053,10 @@ async function startSession(validated, res) {
359710
360053
  ...deviceId ? { "appium:udid": deviceId } : {},
359711
360054
  ...platformVersion ? { "appium:platformVersion": platformVersion } : {},
359712
360055
  "appium:noReset": true,
360056
+ "appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
360057
+ "appium:uiautomator2ServerLaunchTimeout": 12e4,
360058
+ "appium:uiautomator2ServerInstallTimeout": 12e4,
360059
+ "appium:adbExecTimeout": 12e4,
359713
360060
  "appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
359714
360061
  ...realCaps
359715
360062
  };
@@ -360801,11 +361148,33 @@ var MobileBridgeDriver = class {
360801
361148
  */
360802
361149
  async snapshot() {
360803
361150
  return this.withSessionRecovery("snapshot", async () => {
360804
- const [xml, screenshot, windowSize] = await Promise.all([
360805
- getPageSource(this.sessionPath),
360806
- getScreenshot(this.sessionPath),
360807
- getWindowSize(this.sessionPath)
360808
- ]);
361151
+ let xml = await getPageSource(this.sessionPath);
361152
+ let screenshot = await getScreenshot(this.sessionPath);
361153
+ let windowSize = await getWindowSize(this.sessionPath);
361154
+ if (!xml) {
361155
+ try {
361156
+ await waitForUiSettle(this.sessionPath, {
361157
+ timeoutMs: 8e3,
361158
+ minStableFrames: 2,
361159
+ pollMs: 300,
361160
+ minElements: 1
361161
+ });
361162
+ } catch (err) {
361163
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
361164
+ }
361165
+ xml = await getPageSource(this.sessionPath);
361166
+ }
361167
+ if (!xml) {
361168
+ try {
361169
+ await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
361170
+ await waitForColdBootSettle(this.sessionPath);
361171
+ } catch (err) {
361172
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
361173
+ }
361174
+ xml = await getPageSource(this.sessionPath);
361175
+ if (!screenshot) screenshot = await getScreenshot(this.sessionPath);
361176
+ if (!windowSize) windowSize = await getWindowSize(this.sessionPath);
361177
+ }
360809
361178
  if (!xml && !screenshot && !windowSize) {
360810
361179
  await this.assertSessionReachable();
360811
361180
  }
@@ -363745,14 +364114,17 @@ ${finalVerifyResult.logs ?? ""}`;
363745
364114
  // Android default) — a personal physical device must never have
363746
364115
  // its app data wiped.
363747
364116
  allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
363748
- maxSessionRecoveries: 1,
364117
+ // Dual-device discovery + long AI turns can lose WDA/UIA2 more than
364118
+ // once per run. One recovery was too tight — the next snapshot
364119
+ // failed as "no UI tree" after a single WDA death.
364120
+ maxSessionRecoveries: 3,
363749
364121
  recoverSession: async (cause) => {
363750
364122
  if (discoveryTeardownStarted) {
363751
364123
  throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
363752
364124
  }
363753
364125
  const lost = session;
363754
364126
  mobileOnLog(
363755
- `[appium] discovery session lost (${cause.message}); recreating once on the leased device`
364127
+ `[appium] discovery session lost (${cause.message}); recreating on the leased device`
363756
364128
  );
363757
364129
  if (lost) {
363758
364130
  detachMirrorSession(lost.deviceId);