@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.js CHANGED
@@ -975,10 +975,11 @@ var require_mobile_source_parser = __commonJS({
975
975
  function extractIOS(attrs) {
976
976
  const role = nonEmpty3(attrs.type) ?? "XCUIElementTypeOther";
977
977
  const name = nonEmpty3(attrs.name);
978
+ const identifier = nonEmpty3(attrs.identifier) ?? nonEmpty3(attrs.accessibilityIdentifier);
978
979
  const labelAttr = nonEmpty3(attrs.label);
979
980
  const value = nonEmpty3(attrs.value);
980
981
  const label = name ?? labelAttr ?? value;
981
- const accessibilityId = name;
982
+ const accessibilityId = identifier ?? name;
982
983
  const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
983
984
  const bounds = parseIOSBounds(attrs);
984
985
  const enabled = attrBoolDefaultTrue(attrs.enabled);
@@ -75942,6 +75943,34 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
75942
75943
  latest = boundaryResolved;
75943
75944
  lastSnapshotIteration = iterationForObservation;
75944
75945
  if (boundary.kind === "transient_external") {
75946
+ const isHostHomeShell = /springboard|systemui/i.test(boundary.owner);
75947
+ if (isHostHomeShell) {
75948
+ log2(` Host shell detected from ${source}: ${boundary.owner}; activating ${boundary.targetAppId} so exploration can continue.`);
75949
+ try {
75950
+ await driver.launchApp();
75951
+ const snap = await driver.snapshot();
75952
+ const recovered = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
75953
+ if (recovered) {
75954
+ const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
75955
+ if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
75956
+ const enriched = await enrichResolvedSnapshotWithHybridWeb(recovered, driver, credentialValuesForRedaction, log2, platform3, targetAppId, hybridContextCache, true, source !== "seed snapshot" && source !== "mobile_snapshot");
75957
+ latest = enriched;
75958
+ lastSnapshotIteration = iterationForObservation;
75959
+ const screenId = ingestSnapshot(enriched, iterationForObservation);
75960
+ captureScreenshot(enriched.replacementScreenshot ?? snap.screenshot, screenId);
75961
+ return {
75962
+ observation: enriched.observation,
75963
+ screenId,
75964
+ transientExternal: true,
75965
+ externalOwner: boundary.owner
75966
+ };
75967
+ }
75968
+ }
75969
+ } catch (err) {
75970
+ (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
75971
+ log2(` Failed to recover from host shell ${boundary.owner}: ${err instanceof Error ? err.message : String(err)}`);
75972
+ }
75973
+ }
75945
75974
  log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
75946
75975
  return {
75947
75976
  observation: `${boundaryResolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
@@ -76159,6 +76188,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76159
76188
  let summary = "";
76160
76189
  let truncated = false;
76161
76190
  let truncatedReason;
76191
+ const emptyUiTreeStreak = { count: 0 };
76162
76192
  let finishRedirects = 0;
76163
76193
  let lastFinishBreadthRemainingItems = null;
76164
76194
  let lastFinishBreadthProgressMark = null;
@@ -76497,10 +76527,17 @@ Exploration complete: ${summary}`);
76497
76527
  redactionCredentials: credentialValuesForRedaction,
76498
76528
  authTracker,
76499
76529
  inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
76500
- allowRelaunch: !preserveAppDataForTools
76530
+ allowRelaunch: !preserveAppDataForTools,
76531
+ emptyUiTreeStreak
76501
76532
  });
76502
76533
  pushResult(handled);
76503
76534
  const handledRecord = handled && typeof handled === "object" ? handled : null;
76535
+ if (handledRecord?.permanent === true && handledRecord?.emptyUiTreeStreak) {
76536
+ explorationComplete = true;
76537
+ summary = typeof handledRecord.reason === "string" && handledRecord.reason.trim() ? handledRecord.reason : "Accessibility tree remained empty after repeated Appium observations; exploration stopped early.";
76538
+ log2(`
76539
+ Stopping Phase 1 early: empty UI tree thrash (streak=${handledRecord.emptyUiTreeStreak}).`);
76540
+ }
76504
76541
  const navigationWasAttempted = handledRecord?.ok !== false || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "screenId") || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "observation");
76505
76542
  if (DEVICE_MUTATION_BARRIER_TOOLS.has(toolName) && navigationWasAttempted) {
76506
76543
  navigationBarrierTool = toolName;
@@ -76589,14 +76626,24 @@ Exploration complete: ${summary}`);
76589
76626
  }
76590
76627
  return result;
76591
76628
  }
76629
+ var MAX_EMPTY_UI_TREE_STREAK = 3;
76592
76630
  async function dispatchMobileTool(args) {
76593
- 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;
76631
+ 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;
76632
+ const noteEmptyUiTree = () => {
76633
+ emptyUiTreeStreak.count += 1;
76634
+ return emptyUiTreeStreak.count;
76635
+ };
76636
+ const clearEmptyUiTreeStreak = () => {
76637
+ emptyUiTreeStreak.count = 0;
76638
+ };
76594
76639
  const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
76595
76640
  (0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
76596
76641
  const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
76597
76642
  if (!resolved) {
76643
+ noteEmptyUiTree();
76598
76644
  return Promise.resolve({ observation: "(no UI tree returned)", screenId: null });
76599
76645
  }
76646
+ clearEmptyUiTreeStreak();
76600
76647
  return absorbResolvedObservation(resolved, actionResult.screenshot, iteration, source, preferCredentialSecondary);
76601
76648
  };
76602
76649
  const currentAuthSurface = () => {
@@ -76626,8 +76673,17 @@ Exploration complete: ${summary}`);
76626
76673
  const snap = await driver.snapshot();
76627
76674
  const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
76628
76675
  if (!resolved) {
76629
- return { ok: false, error: "snapshot returned no UI tree" };
76676
+ const streak = noteEmptyUiTree();
76677
+ log2(` Snapshot returned no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
76678
+ return {
76679
+ ok: false,
76680
+ error: "snapshot returned no UI tree",
76681
+ emptyUiTreeStreak: streak,
76682
+ permanent: streak >= MAX_EMPTY_UI_TREE_STREAK,
76683
+ 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."
76684
+ };
76630
76685
  }
76686
+ clearEmptyUiTreeStreak();
76631
76687
  const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_snapshot");
76632
76688
  if (absorbed.screenId) {
76633
76689
  const elementCount = state2.getScreen(absorbed.screenId)?.elements.length ?? resolved.screen.elements.length;
@@ -77000,14 +77056,30 @@ Exploration complete: ${summary}`);
77000
77056
  note: "Relaunch refused under preserve policy."
77001
77057
  };
77002
77058
  }
77059
+ if (emptyUiTreeStreak.count >= MAX_EMPTY_UI_TREE_STREAK) {
77060
+ return {
77061
+ ok: false,
77062
+ 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.`,
77063
+ permanent: true,
77064
+ emptyUiTreeStreak: emptyUiTreeStreak.count
77065
+ };
77066
+ }
77003
77067
  authTracker?.noteInterveningMutation(iteration);
77004
77068
  await driver.relaunch();
77005
77069
  log2(" Relaunched app for hermetic reset.");
77006
77070
  const snap = await driver.snapshot();
77007
77071
  const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
77008
77072
  if (!resolved) {
77009
- return { ok: true, note: "relaunched; no UI tree returned \u2014 call mobile_snapshot." };
77073
+ const streak = noteEmptyUiTree();
77074
+ log2(` Relaunch yielded no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
77075
+ return {
77076
+ ok: true,
77077
+ note: "relaunched; no UI tree returned \u2014 call mobile_snapshot.",
77078
+ emptyUiTreeStreak: streak,
77079
+ permanent: streak >= MAX_EMPTY_UI_TREE_STREAK
77080
+ };
77010
77081
  }
77082
+ clearEmptyUiTreeStreak();
77011
77083
  const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_relaunch");
77012
77084
  return {
77013
77085
  ok: true,
@@ -80239,6 +80311,10 @@ var require_mobile_discovery_prompts = __commonJS({
80239
80311
  "use strict";
80240
80312
  Object.defineProperty(exports2, "__esModule", { value: true });
80241
80313
  exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = void 0;
80314
+ exports2.isScrubbedPlaceholderName = isScrubbedPlaceholderName;
80315
+ exports2.isOpaqueMobileScreenName = isOpaqueMobileScreenName;
80316
+ exports2.isPackageLeafAppCatchAll = isPackageLeafAppCatchAll;
80317
+ exports2.isLowValueMobileAutoDeepFeature = isLowValueMobileAutoDeepFeature;
80242
80318
  exports2.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
80243
80319
  exports2.transitionSourceLabel = transitionSourceLabel;
80244
80320
  exports2.buildMobileSiteMap = buildMobileSiteMap;
@@ -80519,6 +80595,32 @@ Each entry is exactly one of:
80519
80595
  }
80520
80596
  return /^[a-z][\w$]*(?:\.[a-z][\w$]*)+(?:#[a-z0-9_. -]+)?$/i.test(trimmed);
80521
80597
  }
80598
+ function isPackageLeafAppCatchAll(name, appId) {
80599
+ const trimmed = name.trim();
80600
+ if (!trimmed || !appId?.trim())
80601
+ return false;
80602
+ const match = trimmed.match(/^(.+?)\s+app$/i);
80603
+ if (!match)
80604
+ return false;
80605
+ const brand = match[1].trim();
80606
+ if (!brand || /\s/.test(brand))
80607
+ return false;
80608
+ const leaf = appId.split(".").map((p) => p.trim()).filter(Boolean).pop();
80609
+ if (!leaf)
80610
+ return false;
80611
+ const normalize = (s) => s.replace(/[^a-z0-9]/gi, "").toLowerCase();
80612
+ return normalize(brand) === normalize(leaf);
80613
+ }
80614
+ function isLowValueMobileAutoDeepFeature(name, appId) {
80615
+ const trimmed = name.trim();
80616
+ if (!trimmed)
80617
+ return true;
80618
+ if (isScrubbedPlaceholderName(trimmed) || isOpaqueMobileScreenName(trimmed, appId))
80619
+ return true;
80620
+ if (isPackageLeafAppCatchAll(trimmed, appId))
80621
+ return true;
80622
+ return false;
80623
+ }
80522
80624
  function humanizeToken(token) {
80523
80625
  const spaced = token.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
80524
80626
  if (!spaced)
@@ -81550,6 +81652,7 @@ var require_mobile_generator = __commonJS({
81550
81652
  exports2.coalesceRepeatedTaps = coalesceRepeatedTaps;
81551
81653
  exports2.elideNoOpDetours = elideNoOpDetours;
81552
81654
  exports2.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
81655
+ exports2.isEphemeralContentAssertText = isEphemeralContentAssertText;
81553
81656
  exports2.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
81554
81657
  exports2.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
81555
81658
  exports2.runMobileGeneratePhase = runMobileGeneratePhase;
@@ -82010,15 +82113,20 @@ ${hybridSkeleton.join("\n")}`;
82010
82113
  - 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.
82011
82114
  - 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.
82012
82115
 
82013
- ## Your job
82116
+ ## Your job \u2014 feature fidelity (non-negotiable)
82117
+ 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.
82118
+
82014
82119
  1. mobile_snapshot the current screen.
82015
- 2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
82016
- 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.
82017
- 4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
82018
- - mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
82019
- - mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
82020
- - 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.
82021
- 5. Call finish_generate with verified:true ONLY if you asserted the outcome with an assert tool.
82120
+ 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.
82121
+ 3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way:
82122
+ - 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.
82123
+ - Content actions (like, share, follow, bookmark, download, \u2026): you MUST tap the control that performs that action \u2014 tab thrash / swipes alone are WRONG.
82124
+ - Navigation scenarios: you MUST land on the destination and assert something unique to it.
82125
+ 4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
82126
+ - mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
82127
+ - 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.
82128
+ - Do NOT assert bottom-nav tab labels alone, notification badge counts ("N unread"), or one-time empty-state CTAs as the scenario outcome.
82129
+ 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.
82022
82130
 
82023
82131
  ## Rules
82024
82132
  - Re-snapshot before every assertion so the ref is fresh.
@@ -82069,7 +82177,7 @@ ${stepLines || " (no explicit steps \u2014 explore the goal directly)"}
82069
82177
  Expected outcomes to PROVE with assert tools:
82070
82178
  ${outcomeLines || " - The target screen is reached and a stable element is visible."}
82071
82179
 
82072
- 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.`;
82180
+ 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.`;
82073
82181
  }
82074
82182
  function buildStatePacket(input) {
82075
82183
  const { scenario, iteration, maxIterations, latestObservation, assertCount, actionCount } = input;
@@ -83536,6 +83644,84 @@ ${statePacket}`, redactionCredentials) },
83536
83644
  "element-hidden",
83537
83645
  "screen-visible"
83538
83646
  ]);
83647
+ 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;
83648
+ 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;
83649
+ var VOLATILE_BADGE_ASSERT_RE = /\b\d+\s*(unread|notifications?|messages?|likes?|followers?|comments?|views?)\b|\bunread\b.*\b\d+\b/i;
83650
+ function isContentActionScenario(scenario) {
83651
+ const text = scenarioText(scenario);
83652
+ if (!CONTENT_ACTION_VERB_RE.test(text))
83653
+ return false;
83654
+ if (/\b(view|see|show|open|browse|list|count)\s+(the\s+)?(likes?|followers?|shares?|bookmarks?)\b/i.test(text)) {
83655
+ return false;
83656
+ }
83657
+ return true;
83658
+ }
83659
+ function isContentActionTap(action) {
83660
+ if (action.type !== "TAP")
83661
+ return false;
83662
+ const textForAction = actionText(action);
83663
+ if (NAV_CHROME_TAP_RE.test(textForAction) && !CONTENT_ACTION_VERB_RE.test(textForAction)) {
83664
+ return false;
83665
+ }
83666
+ return CONTENT_ACTION_VERB_RE.test(textForAction);
83667
+ }
83668
+ function isProductiveScenarioAction(action, scenarioTextLower = "") {
83669
+ if (action.type === "TYPE" || action.type === "CLEAR")
83670
+ return true;
83671
+ if (action.type === "TAP") {
83672
+ const t = actionText(action);
83673
+ if (CONTENT_ACTION_VERB_RE.test(t) || isFormCommitAction(action) || isCredentialSubmitTap(action)) {
83674
+ return true;
83675
+ }
83676
+ if (NAV_CHROME_TAP_RE.test(t)) {
83677
+ if (!scenarioTextLower)
83678
+ return false;
83679
+ const chromeWords = t.match(/\b(home|you|profile|search|settings|notifications?|inbox|messages?|explore|library|more|menu|videos?|create|feed)\b/gi) ?? [];
83680
+ return chromeWords.some((w) => scenarioTextLower.includes(w.toLowerCase()));
83681
+ }
83682
+ return true;
83683
+ }
83684
+ return false;
83685
+ }
83686
+ function isNavChromeThrashTrace(actions) {
83687
+ const meaningful = actions.filter((a) => a.type !== "WAIT_FOR_ELEMENT");
83688
+ if (meaningful.length === 0)
83689
+ return false;
83690
+ const hasNavOrGesture = meaningful.some((a) => a.type === "TAP" || a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME");
83691
+ if (!hasNavOrGesture)
83692
+ return false;
83693
+ return meaningful.every((a) => {
83694
+ if (a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME")
83695
+ return true;
83696
+ if (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") {
83697
+ const t = actionText(a);
83698
+ return NAV_CHROME_TAP_RE.test(t) || VOLATILE_BADGE_ASSERT_RE.test(t) || isStructuralOnlyAssert(a);
83699
+ }
83700
+ if (a.type === "TAP") {
83701
+ return NAV_CHROME_TAP_RE.test(actionText(a)) && !CONTENT_ACTION_VERB_RE.test(actionText(a));
83702
+ }
83703
+ return false;
83704
+ });
83705
+ }
83706
+ function isHarnessOrVisibilityScenario(scenario) {
83707
+ const text = scenarioText(scenario);
83708
+ 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);
83709
+ }
83710
+ function requiresFeatureProgress(scenario) {
83711
+ if (isNavigationFocusedScenario(scenario))
83712
+ return false;
83713
+ if (isCancelDiscardScenario(scenario))
83714
+ return false;
83715
+ if (isContentActionScenario(scenario))
83716
+ return true;
83717
+ if (isHarnessOrVisibilityScenario(scenario))
83718
+ return false;
83719
+ const text = scenarioText(scenario);
83720
+ 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);
83721
+ if (!behaviorClaim)
83722
+ return false;
83723
+ 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);
83724
+ }
83539
83725
  function isInspectionOnlyScenario(scenario, actions) {
83540
83726
  if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
83541
83727
  return false;
@@ -83545,16 +83731,25 @@ ${statePacket}`, redactionCredentials) },
83545
83731
  if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
83546
83732
  return false;
83547
83733
  }
83734
+ if (isContentActionScenario(scenario)) {
83735
+ return false;
83736
+ }
83548
83737
  const text = scenarioText(scenario);
83549
- 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);
83738
+ 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);
83550
83739
  if (visibilityOnlyGoal) {
83551
83740
  const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83552
83741
  return assertCount2 > 0;
83553
83742
  }
83554
- 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);
83743
+ 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);
83555
83744
  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);
83556
83745
  if (completionIntent && formIntent)
83557
83746
  return false;
83747
+ if (completionIntent && /\b(like|unlike|share|follow|bookmark|download|subscribe|create|save|send|post|delete|remove|add|play|pause)\b/i.test(text)) {
83748
+ return false;
83749
+ }
83750
+ if (completionIntent) {
83751
+ return false;
83752
+ }
83558
83753
  const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83559
83754
  return assertCount > 0;
83560
83755
  }
@@ -83564,7 +83759,18 @@ ${statePacket}`, redactionCredentials) },
83564
83759
  const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
83565
83760
  return navigates && !completesFlow;
83566
83761
  }
83762
+ function isCancelDiscardScenario(scenario) {
83763
+ const text = scenarioText(scenario);
83764
+ 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)) {
83765
+ return false;
83766
+ }
83767
+ 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);
83768
+ 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);
83769
+ return cancelEditPhrase || discardWithoutSave;
83770
+ }
83567
83771
  function isFormValidationScenario(scenario) {
83772
+ if (isCancelDiscardScenario(scenario))
83773
+ return false;
83568
83774
  if (scenario.variant === "validation")
83569
83775
  return true;
83570
83776
  if (scenario.type !== "EDGE_CASE")
@@ -83575,6 +83781,8 @@ ${statePacket}`, redactionCredentials) },
83575
83781
  function isFormMutationHappyPath(scenario) {
83576
83782
  if (isNavigationFocusedScenario(scenario))
83577
83783
  return false;
83784
+ if (isCancelDiscardScenario(scenario))
83785
+ return false;
83578
83786
  if (isFormValidationScenario(scenario))
83579
83787
  return false;
83580
83788
  const text = scenarioText(scenario);
@@ -83587,9 +83795,14 @@ ${statePacket}`, redactionCredentials) },
83587
83795
  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)) {
83588
83796
  return false;
83589
83797
  }
83590
- const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
83591
- 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);
83592
- return mutates && hasObject;
83798
+ 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)) {
83799
+ return false;
83800
+ }
83801
+ 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";
83802
+ const mutationVerb = "change|update|create|save|send|register|sign\\s*up|checkout|edit|post|write|compose|set|add|delete|remove";
83803
+ const determiner = "(?:(?:the|a|an|my|your|their|this|that)\\s+)?(?:new\\s+)?";
83804
+ const mutatesFormObject = new RegExp(`\\b(?:${mutationVerb})\\s+${determiner}(?:${formObject})\\b`).test(text) || new RegExp(`\\b(?:${formObject})\\s+(?:${mutationVerb})\\b`).test(text);
83805
+ return mutatesFormObject;
83593
83806
  }
83594
83807
  function isNavigationChromeTap(action) {
83595
83808
  if (action.type !== "TAP")
@@ -83678,12 +83891,46 @@ ${statePacket}`, redactionCredentials) },
83678
83891
  }
83679
83892
  function scenarioIntentMismatchReason(scenario, actions) {
83680
83893
  const text = scenarioText(scenario);
83894
+ if (isContentActionScenario(scenario)) {
83895
+ const hasActionTap = actions.some((a) => isContentActionTap(a));
83896
+ if (!hasActionTap) {
83897
+ return "content-action scenario (like/share/follow/\u2026) did not record a tap on a matching control";
83898
+ }
83899
+ if (isNavChromeThrashTrace(actions)) {
83900
+ return "content-action scenario only recorded tab thrash / swipes \u2014 no product action";
83901
+ }
83902
+ const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
83903
+ 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)))) {
83904
+ return "content-action scenario only asserted tab chrome or a volatile badge count \u2014 assert the action outcome instead";
83905
+ }
83906
+ return null;
83907
+ }
83681
83908
  if (isInspectionOnlyScenario(scenario, actions)) {
83682
83909
  return null;
83683
83910
  }
83684
83911
  if (isNavigationFocusedScenario(scenario)) {
83685
83912
  return null;
83686
83913
  }
83914
+ if (isCancelDiscardScenario(scenario)) {
83915
+ const hasCancelTap = actions.some((a) => {
83916
+ if (a.type !== "TAP")
83917
+ return false;
83918
+ return /\b(cancel|discard|close|dismiss|revert|undo|not\s+now)\b/.test(actionText(a));
83919
+ });
83920
+ const lastNavIdx = (() => {
83921
+ for (let i = actions.length - 1; i >= 0; i--) {
83922
+ const t = actions[i].type;
83923
+ if (t === "TAP" || t === "BACK" || t === "TYPE" || t === "CLEAR")
83924
+ return i;
83925
+ }
83926
+ return -1;
83927
+ })();
83928
+ 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);
83929
+ if (!hasCancelTap && !backAsDismiss) {
83930
+ return "cancel/discard scenario did not record a cancel/dismiss action";
83931
+ }
83932
+ return null;
83933
+ }
83687
83934
  if (isFormValidationScenario(scenario)) {
83688
83935
  const typedFields = actions.some((a) => a.type === "TYPE");
83689
83936
  const hasCommit = actions.some((a) => isFormCommitAction(a));
@@ -83777,30 +84024,42 @@ ${statePacket}`, redactionCredentials) },
83777
84024
  }
83778
84025
  }
83779
84026
  const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
83780
- if (!isHappyPath)
83781
- return null;
83782
- let lastTypeIndex = -1;
83783
- let lastAssertIndex = -1;
83784
- actions.forEach((action, index) => {
83785
- if (action.type === "TYPE")
83786
- lastTypeIndex = index;
83787
- if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
83788
- lastAssertIndex = index;
83789
- });
83790
- if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
83791
- return null;
83792
- const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
83793
- const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
83794
- const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
83795
- const lastTypeAction = actions[lastTypeIndex];
83796
- const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
83797
- const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
83798
- if (index <= lastTypeIndex || index >= lastAssertIndex)
83799
- return false;
83800
- return isFormCommitAction(action);
83801
- });
83802
- if (screensDiffer && !commitAfterType) {
83803
- return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
84027
+ if (isHappyPath) {
84028
+ let lastTypeIndex = -1;
84029
+ let lastAssertIndex = -1;
84030
+ actions.forEach((action, index) => {
84031
+ if (action.type === "TYPE")
84032
+ lastTypeIndex = index;
84033
+ if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
84034
+ lastAssertIndex = index;
84035
+ });
84036
+ if (lastTypeIndex >= 0 && lastAssertIndex > lastTypeIndex) {
84037
+ const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
84038
+ const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
84039
+ const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
84040
+ const lastTypeAction = actions[lastTypeIndex];
84041
+ const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
84042
+ const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
84043
+ if (index <= lastTypeIndex || index >= lastAssertIndex)
84044
+ return false;
84045
+ return isFormCommitAction(action);
84046
+ });
84047
+ if (screensDiffer && !commitAfterType) {
84048
+ return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
84049
+ }
84050
+ }
84051
+ }
84052
+ if (isNavChromeThrashTrace(actions) && !isNavigationFocusedScenario(scenario) && !isFormMutationHappyPath(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario && !isHarnessOrVisibilityScenario(scenario)) {
84053
+ return "recording is only bottom-nav thrash and swipes \u2014 does not execute the scenario goal";
84054
+ }
84055
+ if (requiresFeatureProgress(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario) {
84056
+ if (!actions.some((a) => isProductiveScenarioAction(a, text))) {
84057
+ return "happy-path / user-journey scenario did not record a productive product action (only chrome, gestures, or empty work)";
84058
+ }
84059
+ const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
84060
+ if (asserts.length > 0 && asserts.every((a) => VOLATILE_BADGE_ASSERT_RE.test(actionText(a)) || isStructuralOnlyAssert(a))) {
84061
+ return "happy-path / user-journey scenario only asserted volatile badges or structural roots \u2014 assert a durable feature outcome";
84062
+ }
83804
84063
  }
83805
84064
  return null;
83806
84065
  }
@@ -84055,6 +84314,29 @@ ${statePacket}`, redactionCredentials) },
84055
84314
  lines.push("});");
84056
84315
  return lines.join("\n");
84057
84316
  }
84317
+ function isEphemeralContentAssertText(text) {
84318
+ const t = text.trim();
84319
+ if (!t)
84320
+ return false;
84321
+ 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)) {
84322
+ return true;
84323
+ }
84324
+ if (/\b(just\s+now|moments?\s+ago|yesterday)\b/i.test(t))
84325
+ return true;
84326
+ if (/\btoday\s+at\s+\d{1,2}:\d{2}\b/i.test(t))
84327
+ return true;
84328
+ 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);
84329
+ if (feedish && t.length >= 70 && /[.!?].+\s/.test(t) && t.split(/\s+/).length >= 10) {
84330
+ return true;
84331
+ }
84332
+ if (t.length >= 40 && /\b(error\s+\d{3,}|generation failed|rate limit|quota exceeded)\b/i.test(t)) {
84333
+ return true;
84334
+ }
84335
+ return false;
84336
+ }
84337
+ function assertIdentityText(action) {
84338
+ return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
84339
+ }
84058
84340
  function evaluateMobileAssertionStrength(actions) {
84059
84341
  const modelAsserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
84060
84342
  if (modelAsserts.length === 0) {
@@ -84090,6 +84372,24 @@ ${statePacket}`, redactionCredentials) },
84090
84372
  }
84091
84373
  };
84092
84374
  }
84375
+ const ephemeral = modelAsserts.filter((a) => isEphemeralContentAssertText(assertIdentityText(a)));
84376
+ const durable = modelAsserts.filter((a) => !isEphemeralContentAssertText(assertIdentityText(a)));
84377
+ if (ephemeral.length > 0 && durable.length === 0) {
84378
+ return {
84379
+ ok: false,
84380
+ severity: "block",
84381
+ reason: "only ephemeral content assertions (relative times, long feed/notification copy, or backend error strings) \u2014 assert durable chrome or run-scoped data instead",
84382
+ signals: {
84383
+ apiTest: false,
84384
+ interacted: true,
84385
+ mutated: actions.some((a) => a.type === "TYPE"),
84386
+ navOnly: false,
84387
+ hasOutcomeProof: false,
84388
+ errorBanner: false,
84389
+ terminal: null
84390
+ }
84391
+ };
84392
+ }
84093
84393
  const code = mobileActionsToPseudoPlaywright(actions);
84094
84394
  const interacted = actions.some((a) => a.type === "TAP" || a.type === "TYPE" || a.type === "CLEAR");
84095
84395
  return (0, shared_1.validateAssertionStrength)(code, { interactionPerformed: interacted });
@@ -84965,6 +85265,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
84965
85265
  For a fix, return the COMPLETE steps array (not a diff).`;
84966
85266
  var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
84967
85267
  var RECOVERABLE_MAX = 3;
85268
+ var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
84968
85269
  var BONUS_ATTEMPTS_CEIL = 4;
84969
85270
  var DEFAULT_BONUS_ATTEMPTS = 2;
84970
85271
  var MAX_LOG_CHARS = 4096;
@@ -85570,6 +85871,7 @@ ${args.directive}`);
85570
85871
  const triedLocators = [];
85571
85872
  let sameErrorCount = 0;
85572
85873
  let lastErrorSignature = null;
85874
+ let consecutiveInvalidVerdicts = 0;
85573
85875
  const markRecoverableTurn = () => {
85574
85876
  if (recoverableUsed < RECOVERABLE_MAX) {
85575
85877
  attemptCap += 1;
@@ -85643,11 +85945,22 @@ ${args.directive}`);
85643
85945
  aiResponseCount += 1;
85644
85946
  const parsed = parseHealVerdict(content);
85645
85947
  if (!parsed) {
85646
- options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict (${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 granting a recovery turn`);
85948
+ consecutiveInvalidVerdicts += 1;
85949
+ 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)`);
85950
+ if (consecutiveInvalidVerdicts >= MAX_CONSECUTIVE_INVALID_VERDICTS) {
85951
+ options.onProgress?.(`Phase 1 aborted: ${consecutiveInvalidVerdicts} consecutive invalid verdicts \u2014 freeing device`);
85952
+ return {
85953
+ passed: false,
85954
+ verdict: "config_issue",
85955
+ healReason: `heal model returned ${consecutiveInvalidVerdicts} consecutive non-verdict responses (last ${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 aborting to free the device`,
85956
+ attemptsUsed: attempt
85957
+ };
85958
+ }
85647
85959
  markRecoverableTurn();
85648
- pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object.";
85960
+ pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object \u2014 no prose, no markdown fences.";
85649
85961
  continue;
85650
85962
  }
85963
+ consecutiveInvalidVerdicts = 0;
85651
85964
  if (TERMINAL_VERDICTS.has(parsed.verdict)) {
85652
85965
  return { passed: false, verdict: parsed.verdict, healReason: parsed.reason, attemptsUsed: attempt };
85653
85966
  }
@@ -353034,9 +353347,9 @@ var require_dist2 = __commonJS({
353034
353347
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
353035
353348
  };
353036
353349
  Object.defineProperty(exports2, "__esModule", { value: true });
353037
- exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.filterLoginCaptureCredentials = exports2.buildLoginCaptureCredentials = 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.filterFeatureGroupsToPathScope = exports2.isAuthFlowRoute = exports2.deriveSurveyPathScope = exports2.canonicalizeRoute = exports2.codeUsesInboxHelper = exports2.codeUsesTestInboxIdentity = exports2.codeSelfProvisionsRegistration = 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.prepareMobileScenarioAppState = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
353038
- 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.validateAuthoredMobileTest = exports2.buildMobileRepoAuthorSystemRules = exports2.buildMobileRepoAuthorKickoff = exports2.buildMobileRepoAuthorContractMd = 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.MOBILE_IMPORT_SCHEMA_VERSION = exports2.validateImportedMobileTest = exports2.buildMobileImportSystemRules = exports2.buildMobileImportKickoff = exports2.buildMobileImportContractMd = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = void 0;
353039
- exports2.BrokeredInboxReader = exports2.applyPlusTag = exports2.deriveProjectInboxTag = exports2.deriveInboxTag = exports2.loadVerificationInboxConfig = exports2.VerificationInboxReader = exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.truncateAtLine = exports2.neutralizeUntrustedBlock = exports2.renderUntrustedKnowledgeBlock = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = void 0;
353350
+ exports2.filterLoginCaptureCredentials = exports2.buildLoginCaptureCredentials = 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.filterFeatureGroupsToPathScope = exports2.isAuthFlowRoute = exports2.deriveSurveyPathScope = exports2.canonicalizeRoute = exports2.codeUsesInboxHelper = exports2.codeUsesTestInboxIdentity = exports2.codeSelfProvisionsRegistration = 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.prepareMobileScenarioAppState = exports2.runMobileGeneratePhase = exports2.isLowValueMobileAutoDeepFeature = exports2.isPackageLeafAppCatchAll = exports2.isOpaqueMobileScreenName = exports2.isScrubbedPlaceholderName = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
353351
+ exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.validateAuthoredMobileTest = exports2.buildMobileRepoAuthorSystemRules = exports2.buildMobileRepoAuthorKickoff = exports2.buildMobileRepoAuthorContractMd = 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.MOBILE_IMPORT_SCHEMA_VERSION = exports2.validateImportedMobileTest = exports2.buildMobileImportSystemRules = exports2.buildMobileImportKickoff = exports2.buildMobileImportContractMd = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = 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 = void 0;
353352
+ exports2.BrokeredInboxReader = exports2.applyPlusTag = exports2.deriveProjectInboxTag = exports2.deriveInboxTag = exports2.loadVerificationInboxConfig = exports2.VerificationInboxReader = exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.truncateAtLine = exports2.neutralizeUntrustedBlock = exports2.renderUntrustedKnowledgeBlock = 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 = void 0;
353040
353353
  __exportStar(require_ai_provider(), exports2);
353041
353354
  __exportStar(require_mobile_types(), exports2);
353042
353355
  __exportStar(require_mobile_source_parser(), exports2);
@@ -353061,6 +353374,18 @@ var require_dist2 = __commonJS({
353061
353374
  Object.defineProperty(exports2, "runMobileAIPlanner", { enumerable: true, get: function() {
353062
353375
  return mobile_discovery_prompts_js_1.runMobileAIPlanner;
353063
353376
  } });
353377
+ Object.defineProperty(exports2, "isScrubbedPlaceholderName", { enumerable: true, get: function() {
353378
+ return mobile_discovery_prompts_js_1.isScrubbedPlaceholderName;
353379
+ } });
353380
+ Object.defineProperty(exports2, "isOpaqueMobileScreenName", { enumerable: true, get: function() {
353381
+ return mobile_discovery_prompts_js_1.isOpaqueMobileScreenName;
353382
+ } });
353383
+ Object.defineProperty(exports2, "isPackageLeafAppCatchAll", { enumerable: true, get: function() {
353384
+ return mobile_discovery_prompts_js_1.isPackageLeafAppCatchAll;
353385
+ } });
353386
+ Object.defineProperty(exports2, "isLowValueMobileAutoDeepFeature", { enumerable: true, get: function() {
353387
+ return mobile_discovery_prompts_js_1.isLowValueMobileAutoDeepFeature;
353388
+ } });
353064
353389
  var mobile_generator_js_1 = require_mobile_generator();
353065
353390
  Object.defineProperty(exports2, "runMobileGeneratePhase", { enumerable: true, get: function() {
353066
353391
  return mobile_generator_js_1.runMobileGeneratePhase;
@@ -353496,7 +353821,7 @@ var require_package4 = __commonJS({
353496
353821
  "package.json"(exports2, module2) {
353497
353822
  module2.exports = {
353498
353823
  name: "@validate.qa/runner",
353499
- version: "1.0.85",
353824
+ version: "1.0.86",
353500
353825
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353501
353826
  bin: {
353502
353827
  "validate-runner": "dist/cli.js"
@@ -355408,7 +355733,7 @@ async function pressHome(sessionPath, platform3) {
355408
355733
  // src/services/device-quarantine.ts
355409
355734
  var QUARANTINE_MAX_MS = (() => {
355410
355735
  const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
355411
- return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
355736
+ return Number.isFinite(raw) && raw > 0 ? raw : 2 * 60 * 1e3;
355412
355737
  })();
355413
355738
  var quarantinedAt = /* @__PURE__ */ new Map();
355414
355739
  var nextQuarantineToken = 1;
@@ -356905,6 +357230,12 @@ async function createSession(target) {
356905
357230
  // Scoped to simulator UDIDs (a physical iOS device has no such step);
356906
357231
  // override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
356907
357232
  ...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
357233
+ // WDA cold-start is flaky under dual-sim load (xcodebuild 65/75, SIGTERM).
357234
+ // Retry startup instead of failing the whole discovery session once.
357235
+ "appium:wdaStartupRetries": 4,
357236
+ "appium:wdaStartupRetryInterval": 2e3,
357237
+ "appium:wdaLaunchTimeout": 12e4,
357238
+ "appium:wdaConnectionTimeout": 12e4,
356908
357239
  ...parallelCaps,
356909
357240
  ...realCaps
356910
357241
  } : {
@@ -356916,6 +357247,12 @@ async function createSession(target) {
356916
357247
  "appium:noReset": true,
356917
357248
  // See the iOS branch: the 60s default kills AI-driven discovery sessions.
356918
357249
  "appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
357250
+ // Default UIA2 server launch is 30s — under dual-emu / post-quarantine
357251
+ // stress instrumentation often needs longer or the session never binds
357252
+ // ("instrumentation process cannot be initialized within 30000ms").
357253
+ "appium:uiautomator2ServerLaunchTimeout": 12e4,
357254
+ "appium:uiautomator2ServerInstallTimeout": 12e4,
357255
+ "appium:adbExecTimeout": 12e4,
356919
357256
  "appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
356920
357257
  ...parallelCaps,
356921
357258
  ...realCaps
@@ -359694,6 +360031,12 @@ async function startSession(validated, res) {
359694
360031
  // overrides. Verified live: without this, recorder/mirror sessions on iOS
359695
360032
  // die with "Simulator.app does not exist" while executor sessions work.
359696
360033
  ...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
360034
+ // Match executor createSession: tolerate WDA cold-start flakes.
360035
+ "appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
360036
+ "appium:wdaStartupRetries": 4,
360037
+ "appium:wdaStartupRetryInterval": 2e3,
360038
+ "appium:wdaLaunchTimeout": 12e4,
360039
+ "appium:wdaConnectionTimeout": 12e4,
359697
360040
  ...realCaps
359698
360041
  } : {
359699
360042
  platformName: "Android",
@@ -359702,6 +360045,10 @@ async function startSession(validated, res) {
359702
360045
  ...deviceId ? { "appium:udid": deviceId } : {},
359703
360046
  ...platformVersion ? { "appium:platformVersion": platformVersion } : {},
359704
360047
  "appium:noReset": true,
360048
+ "appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
360049
+ "appium:uiautomator2ServerLaunchTimeout": 12e4,
360050
+ "appium:uiautomator2ServerInstallTimeout": 12e4,
360051
+ "appium:adbExecTimeout": 12e4,
359705
360052
  "appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
359706
360053
  ...realCaps
359707
360054
  };
@@ -360793,11 +361140,33 @@ var MobileBridgeDriver = class {
360793
361140
  */
360794
361141
  async snapshot() {
360795
361142
  return this.withSessionRecovery("snapshot", async () => {
360796
- const [xml, screenshot, windowSize] = await Promise.all([
360797
- getPageSource(this.sessionPath),
360798
- getScreenshot(this.sessionPath),
360799
- getWindowSize(this.sessionPath)
360800
- ]);
361143
+ let xml = await getPageSource(this.sessionPath);
361144
+ let screenshot = await getScreenshot(this.sessionPath);
361145
+ let windowSize = await getWindowSize(this.sessionPath);
361146
+ if (!xml) {
361147
+ try {
361148
+ await waitForUiSettle(this.sessionPath, {
361149
+ timeoutMs: 8e3,
361150
+ minStableFrames: 2,
361151
+ pollMs: 300,
361152
+ minElements: 1
361153
+ });
361154
+ } catch (err) {
361155
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
361156
+ }
361157
+ xml = await getPageSource(this.sessionPath);
361158
+ }
361159
+ if (!xml) {
361160
+ try {
361161
+ await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
361162
+ await waitForColdBootSettle(this.sessionPath);
361163
+ } catch (err) {
361164
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
361165
+ }
361166
+ xml = await getPageSource(this.sessionPath);
361167
+ if (!screenshot) screenshot = await getScreenshot(this.sessionPath);
361168
+ if (!windowSize) windowSize = await getWindowSize(this.sessionPath);
361169
+ }
360801
361170
  if (!xml && !screenshot && !windowSize) {
360802
361171
  await this.assertSessionReachable();
360803
361172
  }
@@ -363737,14 +364106,17 @@ ${finalVerifyResult.logs ?? ""}`;
363737
364106
  // Android default) — a personal physical device must never have
363738
364107
  // its app data wiped.
363739
364108
  allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
363740
- maxSessionRecoveries: 1,
364109
+ // Dual-device discovery + long AI turns can lose WDA/UIA2 more than
364110
+ // once per run. One recovery was too tight — the next snapshot
364111
+ // failed as "no UI tree" after a single WDA death.
364112
+ maxSessionRecoveries: 3,
363741
364113
  recoverSession: async (cause) => {
363742
364114
  if (discoveryTeardownStarted) {
363743
364115
  throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
363744
364116
  }
363745
364117
  const lost = session;
363746
364118
  mobileOnLog(
363747
- `[appium] discovery session lost (${cause.message}); recreating once on the leased device`
364119
+ `[appium] discovery session lost (${cause.message}); recreating on the leased device`
363748
364120
  );
363749
364121
  if (lost) {
363750
364122
  detachMirrorSession(lost.deviceId);