@validate.qa/runner 1.0.84 → 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.
- package/dist/cli.js +635 -88
- package/dist/cli.mjs +635 -88
- 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);
|
|
@@ -1024,7 +1025,8 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1024
1025
|
const isGenericView = /android\.view\.View$/i.test(role);
|
|
1025
1026
|
const moderateHitTarget = hasArea && bounds.width >= 24 && bounds.height >= 24 && bounds.width <= 2e3 && bounds.height <= 1800;
|
|
1026
1027
|
const webHybridLabelledControl = isGenericView && hasHumanIdentity && moderateHitTarget && (focusable || clickable || longClickable || Boolean(contentDesc));
|
|
1027
|
-
const
|
|
1028
|
+
const isAnrChrome = typeof resourceId === "string" && /(?:android:id\/)?aerr_/i.test(resourceId) || typeof text === "string" && /isn['’]?t responding/i.test(text) || typeof label === "string" && /isn['’]?t responding/i.test(label);
|
|
1029
|
+
const actionable = !isAnrChrome && enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
|
|
1028
1030
|
return {
|
|
1029
1031
|
role,
|
|
1030
1032
|
label,
|
|
@@ -1202,6 +1204,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1202
1204
|
element.secure = fields.secure;
|
|
1203
1205
|
if (fields.enabled !== void 0)
|
|
1204
1206
|
element.enabled = fields.enabled;
|
|
1207
|
+
element.visible = fields.visible;
|
|
1205
1208
|
if (isCredibleStructuralNavigationTarget(element) && chainHasNativeNavigationSemantics(chain, platform3)) {
|
|
1206
1209
|
markNavigation(element, "native-navigation-semantics");
|
|
1207
1210
|
}
|
|
@@ -1576,6 +1579,8 @@ var require_mobile_boundary = __commonJS({
|
|
|
1576
1579
|
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports) {
|
|
1577
1580
|
"use strict";
|
|
1578
1581
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1582
|
+
exports.isAndroidAnrSystemResourceId = isAndroidAnrSystemResourceId;
|
|
1583
|
+
exports.looksLikeAndroidAnrUi = looksLikeAndroidAnrUi;
|
|
1579
1584
|
exports.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1580
1585
|
exports.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1581
1586
|
exports.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
@@ -1593,6 +1598,28 @@ var require_mobile_boundary = __commonJS({
|
|
|
1593
1598
|
"com.miui.securitycenter",
|
|
1594
1599
|
"com.samsung.android.permissioncontroller"
|
|
1595
1600
|
]);
|
|
1601
|
+
var ANDROID_ANR_RESOURCE_ID_RE = /^(?:android:id\/)?(?:aerr_(?:wait|close|close_app|mute|report|restart)|alertTitle|button1|button2|button3|parentPanel|topPanel|contentPanel|customPanel|buttonPanel|scrollView|textSpacerNoButtons|textSpacerNoTitle)$/i;
|
|
1602
|
+
var ANDROID_ANR_TEXT_RE = /isn['’]?t responding|isn['’]?t responding\.|anr|close app|wait/i;
|
|
1603
|
+
function isAndroidAnrSystemResourceId(resourceId) {
|
|
1604
|
+
const rid = resourceId?.trim();
|
|
1605
|
+
if (!rid)
|
|
1606
|
+
return false;
|
|
1607
|
+
if (/(?:android:id\/)?aerr_/i.test(rid))
|
|
1608
|
+
return true;
|
|
1609
|
+
return ANDROID_ANR_RESOURCE_ID_RE.test(rid) && /aerr_/i.test(rid);
|
|
1610
|
+
}
|
|
1611
|
+
function looksLikeAndroidAnrUi(input) {
|
|
1612
|
+
if (isAndroidAnrSystemResourceId(input.resourceId))
|
|
1613
|
+
return true;
|
|
1614
|
+
const rid = input.resourceId?.trim() ?? "";
|
|
1615
|
+
const blob = [input.text, input.label, input.contentDesc].filter(Boolean).join(" ");
|
|
1616
|
+
if (/(?:android:id\/)?(?:alertTitle|aerr_)/i.test(rid) && ANDROID_ANR_TEXT_RE.test(blob)) {
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
if (/isn['’]?t responding/i.test(blob))
|
|
1620
|
+
return true;
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1596
1623
|
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1597
1624
|
"com.apple.springboard",
|
|
1598
1625
|
"com.apple.authenticationservicesui",
|
|
@@ -1750,7 +1777,7 @@ var init_constants = __esm({
|
|
|
1750
1777
|
MOBILE_APP_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,253}[A-Za-z0-9])?$/;
|
|
1751
1778
|
MOBILE_FOCUS_SETTLE_MS = 150;
|
|
1752
1779
|
MOBILE_POST_ACTION_SETTLE_MS = 1800;
|
|
1753
|
-
MOBILE_COLD_BOOT_SETTLE_MS =
|
|
1780
|
+
MOBILE_COLD_BOOT_SETTLE_MS = 25e3;
|
|
1754
1781
|
MOBILE_DEFAULT_SWIPE_DISTANCE = 500;
|
|
1755
1782
|
MOBILE_MAX_SWIPE_DISTANCE = 2e3;
|
|
1756
1783
|
SELECTOR_PRIORITIES = [
|
|
@@ -74892,7 +74919,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74892
74919
|
type: "function",
|
|
74893
74920
|
function: {
|
|
74894
74921
|
name: "mobile_relaunch",
|
|
74895
|
-
description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds).
|
|
74922
|
+
description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds). Only for CLEAR_APP_DATA / cold flows. Blocked when app data is being preserved (PRESERVE_APP_DATA or survey AUTO). Do not use on a working product screen. Re-snapshot afterward.",
|
|
74896
74923
|
parameters: { type: "object", properties: {} }
|
|
74897
74924
|
}
|
|
74898
74925
|
};
|
|
@@ -75692,11 +75719,11 @@ Rules:
|
|
|
75692
75719
|
**Phase 2 \u2014 Logged-in breadth first (~35% of budget).** Once signed in, find the primary navigation (bottom tab bar, drawer, or menu) and visit EVERY top-level destination ONCE, observing each screen, BEFORE drilling into any of them. A survey that maps one tab deeply but never opens the other tabs has failed \u2014 breadth across all tabs/drawer entries is mandatory.
|
|
75693
75720
|
**Phase 3 \u2014 Depth (remaining budget).** Return to each observed tab and explore its sub-screens: open ONE list row per list (rows share a template), open menus/sheets/dialogs, observe detail screens, and mobile_swipe up/down to reveal off-screen content. record_transition whenever you move between screens.
|
|
75694
75721
|
|
|
75695
|
-
Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return
|
|
75722
|
+
Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return. Prefer staying on the current app instance \u2014 do not cold-relaunch unless the product UI is truly stuck after a long wait. When no new screens or safe navigation controls remain, call finish_exploration.
|
|
75696
75723
|
|
|
75697
75724
|
## Boundaries (survey = map, do not mutate)
|
|
75698
75725
|
- Do NOT submit forms, save, delete, purchase, or send anything. You MAY open modals/menus to catalog them, then dismiss.
|
|
75699
|
-
- Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back
|
|
75726
|
+
- Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back (not a cold relaunch).
|
|
75700
75727
|
- If you hit a login wall and have no credentials, capture the auth screen and explore what is reachable.
|
|
75701
75728
|
|
|
75702
75729
|
Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
@@ -75921,6 +75948,34 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75921
75948
|
latest = boundaryResolved;
|
|
75922
75949
|
lastSnapshotIteration = iterationForObservation;
|
|
75923
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
|
+
}
|
|
75924
75979
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
75925
75980
|
return {
|
|
75926
75981
|
observation: `${boundaryResolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
@@ -76107,6 +76162,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76107
76162
|
const kickoff = buildKickoffMessage(mode);
|
|
76108
76163
|
const recentExchanges = [];
|
|
76109
76164
|
const transientDirectives = [];
|
|
76165
|
+
const resetPolicyForTools = ctx.target?.resetPolicy ?? "AUTO";
|
|
76166
|
+
const preserveAppDataForTools = resetPolicyForTools === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicyForTools === "AUTO";
|
|
76110
76167
|
const tools = [
|
|
76111
76168
|
MOBILE_SNAPSHOT_TOOL,
|
|
76112
76169
|
MOBILE_TAP_TOOL,
|
|
@@ -76116,7 +76173,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76116
76173
|
MOBILE_SWIPE_TOOL,
|
|
76117
76174
|
MOBILE_BACK_TOOL,
|
|
76118
76175
|
MOBILE_HOME_TOOL,
|
|
76119
|
-
|
|
76176
|
+
// Omit relaunch under preserve — model otherwise cold-restarts RN into splash.
|
|
76177
|
+
...preserveAppDataForTools ? [] : [MOBILE_RELAUNCH_TOOL],
|
|
76120
76178
|
MOBILE_OPEN_DEEPLINK_TOOL,
|
|
76121
76179
|
CAPTURE_SCREEN_TOOL,
|
|
76122
76180
|
RECORD_TRANSITION_TOOL,
|
|
@@ -76135,6 +76193,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76135
76193
|
let summary = "";
|
|
76136
76194
|
let truncated = false;
|
|
76137
76195
|
let truncatedReason;
|
|
76196
|
+
const emptyUiTreeStreak = { count: 0 };
|
|
76138
76197
|
let finishRedirects = 0;
|
|
76139
76198
|
let lastFinishBreadthRemainingItems = null;
|
|
76140
76199
|
let lastFinishBreadthProgressMark = null;
|
|
@@ -76472,10 +76531,18 @@ Exploration complete: ${summary}`);
|
|
|
76472
76531
|
credentials: credentialSecrets,
|
|
76473
76532
|
redactionCredentials: credentialValuesForRedaction,
|
|
76474
76533
|
authTracker,
|
|
76475
|
-
inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null
|
|
76534
|
+
inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
|
|
76535
|
+
allowRelaunch: !preserveAppDataForTools,
|
|
76536
|
+
emptyUiTreeStreak
|
|
76476
76537
|
});
|
|
76477
76538
|
pushResult(handled);
|
|
76478
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
|
+
}
|
|
76479
76546
|
const navigationWasAttempted = handledRecord?.ok !== false || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "screenId") || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "observation");
|
|
76480
76547
|
if (DEVICE_MUTATION_BARRIER_TOOLS.has(toolName) && navigationWasAttempted) {
|
|
76481
76548
|
navigationBarrierTool = toolName;
|
|
@@ -76564,14 +76631,24 @@ Exploration complete: ${summary}`);
|
|
|
76564
76631
|
}
|
|
76565
76632
|
return result;
|
|
76566
76633
|
}
|
|
76634
|
+
var MAX_EMPTY_UI_TREE_STREAK = 3;
|
|
76567
76635
|
async function dispatchMobileTool(args) {
|
|
76568
|
-
const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox } = 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
|
+
};
|
|
76569
76644
|
const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
|
|
76570
76645
|
(0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
|
|
76571
76646
|
const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
|
|
76572
76647
|
if (!resolved) {
|
|
76648
|
+
noteEmptyUiTree();
|
|
76573
76649
|
return Promise.resolve({ observation: "(no UI tree returned)", screenId: null });
|
|
76574
76650
|
}
|
|
76651
|
+
clearEmptyUiTreeStreak();
|
|
76575
76652
|
return absorbResolvedObservation(resolved, actionResult.screenshot, iteration, source, preferCredentialSecondary);
|
|
76576
76653
|
};
|
|
76577
76654
|
const currentAuthSurface = () => {
|
|
@@ -76601,8 +76678,17 @@ Exploration complete: ${summary}`);
|
|
|
76601
76678
|
const snap = await driver.snapshot();
|
|
76602
76679
|
const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
|
|
76603
76680
|
if (!resolved) {
|
|
76604
|
-
|
|
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
|
+
};
|
|
76605
76690
|
}
|
|
76691
|
+
clearEmptyUiTreeStreak();
|
|
76606
76692
|
const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_snapshot");
|
|
76607
76693
|
if (absorbed.screenId) {
|
|
76608
76694
|
const elementCount = state2.getScreen(absorbed.screenId)?.elements.length ?? resolved.screen.elements.length;
|
|
@@ -76968,14 +77054,37 @@ Exploration complete: ${summary}`);
|
|
|
76968
77054
|
}
|
|
76969
77055
|
// ----- Relaunch (hermetic reset) -----
|
|
76970
77056
|
case "mobile_relaunch": {
|
|
77057
|
+
if (!allowRelaunch) {
|
|
77058
|
+
return {
|
|
77059
|
+
ok: false,
|
|
77060
|
+
error: "mobile_relaunch is blocked while app data is being preserved. Use mobile_back / mobile_snapshot to continue mapping; request CLEAR_APP_DATA for a true cold restart.",
|
|
77061
|
+
note: "Relaunch refused under preserve policy."
|
|
77062
|
+
};
|
|
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
|
+
}
|
|
76971
77072
|
authTracker?.noteInterveningMutation(iteration);
|
|
76972
77073
|
await driver.relaunch();
|
|
76973
77074
|
log2(" Relaunched app for hermetic reset.");
|
|
76974
77075
|
const snap = await driver.snapshot();
|
|
76975
77076
|
const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
|
|
76976
77077
|
if (!resolved) {
|
|
76977
|
-
|
|
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
|
+
};
|
|
76978
77086
|
}
|
|
77087
|
+
clearEmptyUiTreeStreak();
|
|
76979
77088
|
const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_relaunch");
|
|
76980
77089
|
return {
|
|
76981
77090
|
ok: true,
|
|
@@ -80207,6 +80316,10 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
80207
80316
|
"use strict";
|
|
80208
80317
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
80209
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;
|
|
80210
80323
|
exports.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
|
|
80211
80324
|
exports.transitionSourceLabel = transitionSourceLabel;
|
|
80212
80325
|
exports.buildMobileSiteMap = buildMobileSiteMap;
|
|
@@ -80260,7 +80373,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80260
80373
|
5. CRUD screens for the same entity belong together
|
|
80261
80374
|
6. Do NOT merge screens into one feature just because they serve a similar broader goal. Treat areas as separate features when the evidence shows separate primary navigation destinations (e.g. separate tab-bar items or drawer links), separate screen families for distinct entities, or different CRUD entities/forms. Keep screens together when they share one navigational area and operate on the same entity (e.g. a list screen, its filter/sort sheet, and its detail screen all belong together).
|
|
80262
80375
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
80263
|
-
8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature.
|
|
80376
|
+
8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature. Never name a feature after a scrub/placeholder token such as "<number>", "<identity>", "<email>", or a bare number \u2014 those are privacy redactions of dynamic content, not product features. Attach those screens to the nearest real product feature instead.
|
|
80264
80377
|
9. COVER EVERY PRODUCT SCREEN: every explored product screen except generic error / permission / empty / loading / intermediate screens MUST appear in exactly one feature group's routes. Do not leave usable screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous usable screen to its most closely related feature and note the ambiguity in that feature's description. Sign-in / sign-up / forgot-password screens form an "Authentication" group even when they were only reached via links from other screens.
|
|
80265
80378
|
10. LOGGED-OUT vs LOGGED-IN SEPARATION: screens shown BEFORE the user is signed in (splash, landing/welcome, theme or language pickers, walkthroughs, sign-up, sign-in, forgot-password, demo/preview screens) must NEVER share a feature group with the signed-in app. Put sign-in / sign-up / forgot-password in "Authentication" (rule 4) and the remaining pre-login screens in an "Onboarding" group. Features named after signed-in areas (feeds, dashboards, tabs, settings) must contain ONLY screens reached after authentication. A pre-login flow is a linear sequence, not a navigation destination \u2014 do not let it swallow the app's main features.
|
|
80266
80379
|
11. OUTPUT SIZE: screen-ids are long opaque values. List each one exactly once in the routes array, but do NOT repeat them in pages or flows. Omit pages and flows; the harness deterministically reconstructs those fields from the captured graph. This is mandatory for large maps so the JSON is not truncated.
|
|
@@ -80305,6 +80418,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80305
80418
|
Example - GOOD happy path (generic, async): \`["Launch app and sign in if needed", "Navigate to the feature screen", "Note the current count of rows in the list", "Tap the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the row count increased by 1", "Open the newly created row", "Verify its content is non-empty and contains the input I provided"]\`
|
|
80306
80419
|
Example - GOOD happy path (sync CRUD): \`["Launch app and sign in if needed", "Navigate to the list screen", "Tap New", "Fill required fields", "Tap Save", "Verify the new record appears in the list", "Tap the record and verify its detail screen shows the values I entered"]\`
|
|
80307
80420
|
13. **Auth happy paths require concrete session proof.** A sign-in HAPPY_PATH is incomplete if it only proves "user reaches the home screen" or "an authenticated screen is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: the account/profile tab opens, Sign out is available, the observed display name/email/avatar is present, an authenticated screen remains reachable after a relaunch with the session restored, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Sign out", "Account", "Profile", or the user's own displayed name/email) over generic phrases like "personalized content." If the explorer only observed a screen transition and no signed-in signal, keep the screen outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
|
|
80421
|
+
13b. **Session-mode match \u2014 no guest-wall scenarios when the explore session was authenticated.** If the explorer evidence shows authenticated product screens (requiresAuth / post-login provenance / preserved-session product), do NOT plan scenarios that expect a guest-only gate after a product action (e.g. "Like prompts Sign In", "Add to playlist requires Log In", "Download requires Sign In"). Those gates do not appear for the signed-in session the runner will use to re-execute. Prefer the authenticated outcome actually observed (action succeeds, toggle flips to Unlike, sheet/picker opens, share sheet appears). Guest-wall / sign-in-prompt scenarios are valid ONLY when the explorer evidence for THAT action was recorded while signed out, or the scenario is an Authentication feature flow.
|
|
80308
80422
|
14. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and against an app launch state that may be freshly cleared OR may preserve a previous session depending on the device/reset policy. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
|
|
80309
80423
|
15. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation already in the account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the home screen header is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Tap New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
|
|
80310
80424
|
16. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first row", "the latest message", "the last cell", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs against a fresh session or a different account.
|
|
@@ -80459,10 +80573,26 @@ Each entry is exactly one of:
|
|
|
80459
80573
|
function screenDisplayName(state2) {
|
|
80460
80574
|
return state2.screenName?.trim() || state2.screenId;
|
|
80461
80575
|
}
|
|
80576
|
+
function isScrubbedPlaceholderName(value) {
|
|
80577
|
+
const trimmed = value.trim();
|
|
80578
|
+
if (!trimmed)
|
|
80579
|
+
return false;
|
|
80580
|
+
if (/^<[^<>\s]+>$/.test(trimmed))
|
|
80581
|
+
return true;
|
|
80582
|
+
if (/^(number|identity|email|id|url|token|value|secret|date|time)$/i.test(trimmed)) {
|
|
80583
|
+
return true;
|
|
80584
|
+
}
|
|
80585
|
+
const stripped = trimmed.replace(/<[^<>\s]+>/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
|
|
80586
|
+
if (!stripped)
|
|
80587
|
+
return true;
|
|
80588
|
+
return /^(number|identity|email|id|url|token|value|secret|date|time)(\s+(number|identity|email|id|url|token|value|secret|date|time))*$/i.test(stripped);
|
|
80589
|
+
}
|
|
80462
80590
|
function isOpaqueMobileScreenName(value, appId) {
|
|
80463
80591
|
const trimmed = value.trim();
|
|
80464
80592
|
if (!trimmed)
|
|
80465
80593
|
return false;
|
|
80594
|
+
if (isScrubbedPlaceholderName(trimmed))
|
|
80595
|
+
return true;
|
|
80466
80596
|
const lower = trimmed.toLowerCase();
|
|
80467
80597
|
const lowerAppId = appId?.trim().toLowerCase();
|
|
80468
80598
|
if (lowerAppId && (lower === lowerAppId || lower.startsWith(`${lowerAppId}#`) || lower.startsWith(`${lowerAppId}.`))) {
|
|
@@ -80470,6 +80600,32 @@ Each entry is exactly one of:
|
|
|
80470
80600
|
}
|
|
80471
80601
|
return /^[a-z][\w$]*(?:\.[a-z][\w$]*)+(?:#[a-z0-9_. -]+)?$/i.test(trimmed);
|
|
80472
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
|
+
}
|
|
80473
80629
|
function humanizeToken(token) {
|
|
80474
80630
|
const spaced = token.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
80475
80631
|
if (!spaced)
|
|
@@ -80484,11 +80640,12 @@ Each entry is exactly one of:
|
|
|
80484
80640
|
}
|
|
80485
80641
|
function nonGenericScreenName(state2, appId) {
|
|
80486
80642
|
const raw = state2.screenName?.trim();
|
|
80487
|
-
if (!raw || isOpaqueMobileScreenName(raw, appId))
|
|
80643
|
+
if (!raw || isOpaqueMobileScreenName(raw, appId) || isScrubbedPlaceholderName(raw))
|
|
80488
80644
|
return null;
|
|
80489
80645
|
const human = humanizeToken(raw.replace(/(?:Activity|ViewController|Fragment|Screen|Page)$/i, ""));
|
|
80490
|
-
if (!human || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human))
|
|
80646
|
+
if (!human || isScrubbedPlaceholderName(human) || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human)) {
|
|
80491
80647
|
return null;
|
|
80648
|
+
}
|
|
80492
80649
|
return human;
|
|
80493
80650
|
}
|
|
80494
80651
|
function inferMobileFeatureGroupName(states, appId) {
|
|
@@ -81217,6 +81374,45 @@ ${constraints.join("\n")}`);
|
|
|
81217
81374
|
return finalGroups;
|
|
81218
81375
|
}
|
|
81219
81376
|
var WEB_URL_PREFIX_FALLBACK_LOG_RE = /\bFallback:\s*\d+\s+feature groups? from URL-prefix grouping\b/i;
|
|
81377
|
+
function graphHasAuthenticatedProduct(graph) {
|
|
81378
|
+
for (const state2 of graph.screens.values()) {
|
|
81379
|
+
if (state2.requiresAuth === true)
|
|
81380
|
+
return true;
|
|
81381
|
+
if (state2.authProvenance === "post-submit-authenticated" || state2.authProvenance === "preserved-session-product") {
|
|
81382
|
+
return true;
|
|
81383
|
+
}
|
|
81384
|
+
}
|
|
81385
|
+
return false;
|
|
81386
|
+
}
|
|
81387
|
+
function isGuestWallScenario(scenario) {
|
|
81388
|
+
const feature = (scenario.featureGroup ?? "").toLowerCase();
|
|
81389
|
+
if (/auth|sign[\s-]?in|log[\s-]?in|onboarding/.test(feature))
|
|
81390
|
+
return false;
|
|
81391
|
+
const stepText = (scenario.steps ?? []).map((step) => typeof step === "string" ? step : step.action ?? "").join(" ");
|
|
81392
|
+
const outcomeText = (scenario.expectedOutcomes ?? []).map((outcome) => outcome.description ?? "").join(" ");
|
|
81393
|
+
const blob = [scenario.name, scenario.goal, stepText, outcomeText].filter(Boolean).join("\n").toLowerCase();
|
|
81394
|
+
if (/prompt(?:s)?\s+sign[\s-]?in|sign[\s-]?in\s+to\s+\w+|requires?\s+(?:log|sign)[\s-]?in|must\s+(?:log|sign)[\s-]?in|guest\s+(?:only|wall|gate|mode)|log[\s-]?in\s+to\s+(?:continue|like|save|download|comment|follow|share)/i.test(blob)) {
|
|
81395
|
+
return true;
|
|
81396
|
+
}
|
|
81397
|
+
const productAction = /\b(like|unlike|download|playlist|comment|follow|share|clip|save|favorite|bookmark|subscribe|purchase|buy|checkout)\b/i.test(blob);
|
|
81398
|
+
const assertsLoginGate = /\b(?:assert|verify|expect|see|show|display|prompts?)\b[\s\S]{0,40}\b(?:log[\s-]?in|sign[\s-]?in)\b/i.test(blob) || /\b(?:log[\s-]?in|sign[\s-]?in)\b[\s\S]{0,40}\b(?:dialog|sheet|modal|prompt|gate|wall|required)\b/i.test(blob);
|
|
81399
|
+
return productAction && assertsLoginGate;
|
|
81400
|
+
}
|
|
81401
|
+
function filterGuestWallScenariosForAuthSession(scenarios, graph) {
|
|
81402
|
+
if (scenarios.length === 0 || !graphHasAuthenticatedProduct(graph)) {
|
|
81403
|
+
return { scenarios, dropped: 0 };
|
|
81404
|
+
}
|
|
81405
|
+
const kept = [];
|
|
81406
|
+
let dropped = 0;
|
|
81407
|
+
for (const scenario of scenarios) {
|
|
81408
|
+
if (isGuestWallScenario(scenario)) {
|
|
81409
|
+
dropped += 1;
|
|
81410
|
+
continue;
|
|
81411
|
+
}
|
|
81412
|
+
kept.push(scenario);
|
|
81413
|
+
}
|
|
81414
|
+
return { scenarios: kept, dropped };
|
|
81415
|
+
}
|
|
81220
81416
|
function selectMobilePlannerFeatureGroups(result, graph, appId, mode, existingFeatureNames = []) {
|
|
81221
81417
|
if (mode !== "survey") {
|
|
81222
81418
|
return { accepted: result.featureGroups, rejected: [] };
|
|
@@ -81330,7 +81526,12 @@ ${constraints.join("\n")}`);
|
|
|
81330
81526
|
}
|
|
81331
81527
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(plannerGroupsForFiltering, siteMap, input.graph, input.appId, input.mode === "survey");
|
|
81332
81528
|
const mobileFeatureGroups = plannerFeatureGroups.length > 0 ? plannerFeatureGroups : buildMobileFeatureGroupsFromGraph(input.graph, input.appId);
|
|
81333
|
-
|
|
81529
|
+
const guestWallFilter = input.mode !== "survey" ? filterGuestWallScenariosForAuthSession(result.scenarios, input.graph) : { scenarios: result.scenarios, dropped: 0 };
|
|
81530
|
+
if (guestWallFilter.dropped > 0) {
|
|
81531
|
+
mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
|
|
81532
|
+
}
|
|
81533
|
+
const plannedScenarios = guestWallFilter.scenarios;
|
|
81534
|
+
if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
|
|
81334
81535
|
const fallbackScenarios = buildMobileFallbackScenarios(input);
|
|
81335
81536
|
if (fallbackScenarios.length > 0) {
|
|
81336
81537
|
return {
|
|
@@ -81349,6 +81550,7 @@ ${constraints.join("\n")}`);
|
|
|
81349
81550
|
if (input.featureName?.trim()) {
|
|
81350
81551
|
return {
|
|
81351
81552
|
...result,
|
|
81553
|
+
scenarios: plannedScenarios,
|
|
81352
81554
|
featureGroups: mobileFeatureGroups,
|
|
81353
81555
|
logs: [...result.logs, ...mobileAdaptationLogs],
|
|
81354
81556
|
warning: [result.warning, `Deep discovery could not plan any scenarios for "${input.featureName}" \u2014 the explorer did not reach that feature's screens this run. If the feature requires a signed-in session, verify TEST_USER_EMAIL / TEST_USER_PASSWORD are valid for this app, then re-run.`].filter(Boolean).join(" | "),
|
|
@@ -81385,6 +81587,7 @@ ${constraints.join("\n")}`);
|
|
|
81385
81587
|
}
|
|
81386
81588
|
return {
|
|
81387
81589
|
...result,
|
|
81590
|
+
scenarios: plannedScenarios,
|
|
81388
81591
|
featureGroups: mobileFeatureGroups,
|
|
81389
81592
|
logs: [...result.logs, ...mobileAdaptationLogs]
|
|
81390
81593
|
};
|
|
@@ -81454,6 +81657,7 @@ var require_mobile_generator = __commonJS({
|
|
|
81454
81657
|
exports.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81455
81658
|
exports.elideNoOpDetours = elideNoOpDetours;
|
|
81456
81659
|
exports.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81660
|
+
exports.isEphemeralContentAssertText = isEphemeralContentAssertText;
|
|
81457
81661
|
exports.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81458
81662
|
exports.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
81459
81663
|
exports.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
@@ -81914,15 +82118,20 @@ ${hybridSkeleton.join("\n")}`;
|
|
|
81914
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.
|
|
81915
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.
|
|
81916
82120
|
|
|
81917
|
-
## 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
|
+
|
|
81918
82124
|
1. mobile_snapshot the current screen.
|
|
81919
|
-
2. Execute the scenario's steps using the real UI: tap
|
|
81920
|
-
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way
|
|
81921
|
-
|
|
81922
|
-
-
|
|
81923
|
-
-
|
|
81924
|
-
|
|
81925
|
-
|
|
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.
|
|
81926
82135
|
|
|
81927
82136
|
## Rules
|
|
81928
82137
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
@@ -81973,7 +82182,7 @@ ${stepLines || " (no explicit steps \u2014 explore the goal directly)"}
|
|
|
81973
82182
|
Expected outcomes to PROVE with assert tools:
|
|
81974
82183
|
${outcomeLines || " - The target screen is reached and a stable element is visible."}
|
|
81975
82184
|
|
|
81976
|
-
Start by calling mobile_snapshot, then execute the steps
|
|
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.`;
|
|
81977
82186
|
}
|
|
81978
82187
|
function buildStatePacket(input) {
|
|
81979
82188
|
const { scenario, iteration, maxIterations, latestObservation, assertCount, actionCount } = input;
|
|
@@ -82030,7 +82239,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
82030
82239
|
}
|
|
82031
82240
|
}
|
|
82032
82241
|
async function runScenarioLoop(args) {
|
|
82033
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog } = args;
|
|
82242
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
|
|
82034
82243
|
const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
|
|
82035
82244
|
const actions = [];
|
|
82036
82245
|
let assertCount = 0;
|
|
@@ -82604,6 +82813,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82604
82813
|
setLatest,
|
|
82605
82814
|
credentials,
|
|
82606
82815
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82816
|
+
allowRelaunch,
|
|
82607
82817
|
recordAction: (action) => {
|
|
82608
82818
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82609
82819
|
const structuralKey = structuralScreenKey(getLatest());
|
|
@@ -82638,7 +82848,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82638
82848
|
return { actions, assertCount, finish, lastSnapshot: lastTargetSnapshot, lastScreenName: lastTargetScreenName };
|
|
82639
82849
|
}
|
|
82640
82850
|
async function dispatchGenerateTool(args) {
|
|
82641
|
-
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
82851
|
+
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, log: log2 } = args;
|
|
82642
82852
|
const unknownRef = (ref) => ({
|
|
82643
82853
|
ok: false,
|
|
82644
82854
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -83028,6 +83238,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83028
83238
|
return actionPayload(result, absorbed);
|
|
83029
83239
|
}
|
|
83030
83240
|
case "mobile_relaunch": {
|
|
83241
|
+
if (!allowRelaunch) {
|
|
83242
|
+
log2(" relaunch refused under PRESERVE_APP_DATA \u2014 staying on current app instance.");
|
|
83243
|
+
const snap2 = await driver.snapshot();
|
|
83244
|
+
const absorbed2 = await absorbObservation(snap2.xml, snap2.screenSignal, snap2.windowSize, snap2.screenshot, "mobile_relaunch_blocked");
|
|
83245
|
+
return {
|
|
83246
|
+
ok: false,
|
|
83247
|
+
error: "mobile_relaunch is blocked while app data is preserved. Continue from the current screen.",
|
|
83248
|
+
observation: absorbed2.observation
|
|
83249
|
+
};
|
|
83250
|
+
}
|
|
83031
83251
|
await driver.relaunch();
|
|
83032
83252
|
const snap = await driver.snapshot();
|
|
83033
83253
|
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
@@ -83246,14 +83466,48 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83246
83466
|
});
|
|
83247
83467
|
return { ok: true, label: metadata.targetLabel ?? accessibilityId ?? text ?? "element" };
|
|
83248
83468
|
}
|
|
83469
|
+
var AMBIENT_CHROME_LABEL_RE = /^(your\s*profile|you|home|search|profile|more|settings|notifications?|tab\s*\d+(\s*of\s*\d+)?|you,\s*tab.*)$/i;
|
|
83470
|
+
function isDisplayableLandingCandidate(el, windowSize) {
|
|
83471
|
+
if (el.visible === false)
|
|
83472
|
+
return false;
|
|
83473
|
+
const { x, y, width, height } = el.bounds;
|
|
83474
|
+
if (!(width > 0 && height > 0))
|
|
83475
|
+
return false;
|
|
83476
|
+
if (width < 8 || height < 8)
|
|
83477
|
+
return false;
|
|
83478
|
+
if (windowSize && windowSize.width > 0 && windowSize.height > 0) {
|
|
83479
|
+
const centerX = x + width / 2;
|
|
83480
|
+
const centerY = y + height / 2;
|
|
83481
|
+
if (centerX < 0 || centerY < 0 || centerX > windowSize.width || centerY > windowSize.height) {
|
|
83482
|
+
return false;
|
|
83483
|
+
}
|
|
83484
|
+
if (width >= windowSize.width * 0.92 && height >= windowSize.height * 0.55) {
|
|
83485
|
+
return false;
|
|
83486
|
+
}
|
|
83487
|
+
}
|
|
83488
|
+
return true;
|
|
83489
|
+
}
|
|
83490
|
+
function landingAnchorSortKey(el) {
|
|
83491
|
+
const label = (nonEmpty3(el.label) || nonEmpty3(el.text) || "").trim();
|
|
83492
|
+
const ambient = AMBIENT_CHROME_LABEL_RE.test(label) ? 1 : 0;
|
|
83493
|
+
const labelled = nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
|
|
83494
|
+
const area = Math.max(1, el.bounds.width * el.bounds.height);
|
|
83495
|
+
const areaBucket = area < 8e4 ? 0 : area < 25e4 ? 1 : 2;
|
|
83496
|
+
return [ambient, labelled, areaBucket];
|
|
83497
|
+
}
|
|
83249
83498
|
function pickLandingAnchor(snapshot) {
|
|
83250
83499
|
if (!snapshot || snapshot.screen.elements.length === 0)
|
|
83251
83500
|
return null;
|
|
83252
83501
|
const elements = snapshot.screen.elements;
|
|
83253
|
-
const
|
|
83254
|
-
stableAnchors.sort((a, b) => {
|
|
83255
|
-
const
|
|
83256
|
-
|
|
83502
|
+
const windowSize = snapshot.screen.windowSize ?? null;
|
|
83503
|
+
const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)).sort((a, b) => {
|
|
83504
|
+
const ka = landingAnchorSortKey(a);
|
|
83505
|
+
const kb = landingAnchorSortKey(b);
|
|
83506
|
+
for (let i = 0; i < ka.length; i += 1) {
|
|
83507
|
+
if (ka[i] !== kb[i])
|
|
83508
|
+
return ka[i] - kb[i];
|
|
83509
|
+
}
|
|
83510
|
+
return 0;
|
|
83257
83511
|
});
|
|
83258
83512
|
const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83259
83513
|
if (contentfulNonButton)
|
|
@@ -83263,8 +83517,10 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83263
83517
|
return nonButton;
|
|
83264
83518
|
if (stableAnchors.length > 0)
|
|
83265
83519
|
return stableAnchors[0];
|
|
83266
|
-
const labelled = elements.find((el) => !weakAssertionTargetReason(el) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83267
|
-
|
|
83520
|
+
const labelled = elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83521
|
+
if (labelled)
|
|
83522
|
+
return labelled;
|
|
83523
|
+
return elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)) ?? null;
|
|
83268
83524
|
}
|
|
83269
83525
|
var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
|
|
83270
83526
|
var ALWAYS_PRESENT_CONTAINER_ID_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -83362,10 +83618,19 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83362
83618
|
const anchor = pickLandingAnchor(recording.lastSnapshot);
|
|
83363
83619
|
if (!anchor)
|
|
83364
83620
|
return false;
|
|
83621
|
+
const modelAssertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83622
|
+
const anchorLabel = (nonEmpty3(anchor.label) || nonEmpty3(anchor.text) || "").trim();
|
|
83623
|
+
if (modelAssertCount > 0 && AMBIENT_CHROME_LABEL_RE.test(anchorLabel)) {
|
|
83624
|
+
return false;
|
|
83625
|
+
}
|
|
83626
|
+
const anchorSelector = (0, mobile_source_parser_js_2.preferredLocator)(anchor);
|
|
83627
|
+
const alreadyAsserted = actions.some((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") && typeof a.selector === "string" && a.selector === anchorSelector);
|
|
83628
|
+
if (alreadyAsserted)
|
|
83629
|
+
return false;
|
|
83365
83630
|
const screen = recording.lastScreenName || "LandingScreen";
|
|
83366
83631
|
actions.push(finalizeAction({
|
|
83367
83632
|
type: "ASSERT_VISIBLE",
|
|
83368
|
-
selector:
|
|
83633
|
+
selector: anchorSelector,
|
|
83369
83634
|
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83370
83635
|
}));
|
|
83371
83636
|
return true;
|
|
@@ -83384,6 +83649,84 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83384
83649
|
"element-hidden",
|
|
83385
83650
|
"screen-visible"
|
|
83386
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
|
+
}
|
|
83387
83730
|
function isInspectionOnlyScenario(scenario, actions) {
|
|
83388
83731
|
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
83389
83732
|
return false;
|
|
@@ -83393,16 +83736,25 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83393
83736
|
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
83394
83737
|
return false;
|
|
83395
83738
|
}
|
|
83739
|
+
if (isContentActionScenario(scenario)) {
|
|
83740
|
+
return false;
|
|
83741
|
+
}
|
|
83396
83742
|
const text = scenarioText(scenario);
|
|
83397
|
-
const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|
|
|
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);
|
|
83398
83744
|
if (visibilityOnlyGoal) {
|
|
83399
83745
|
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83400
83746
|
return assertCount2 > 0;
|
|
83401
83747
|
}
|
|
83402
|
-
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);
|
|
83403
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);
|
|
83404
83750
|
if (completionIntent && formIntent)
|
|
83405
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
|
+
}
|
|
83406
83758
|
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83407
83759
|
return assertCount > 0;
|
|
83408
83760
|
}
|
|
@@ -83412,7 +83764,18 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83412
83764
|
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
83413
83765
|
return navigates && !completesFlow;
|
|
83414
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
|
+
}
|
|
83415
83776
|
function isFormValidationScenario(scenario) {
|
|
83777
|
+
if (isCancelDiscardScenario(scenario))
|
|
83778
|
+
return false;
|
|
83416
83779
|
if (scenario.variant === "validation")
|
|
83417
83780
|
return true;
|
|
83418
83781
|
if (scenario.type !== "EDGE_CASE")
|
|
@@ -83423,6 +83786,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83423
83786
|
function isFormMutationHappyPath(scenario) {
|
|
83424
83787
|
if (isNavigationFocusedScenario(scenario))
|
|
83425
83788
|
return false;
|
|
83789
|
+
if (isCancelDiscardScenario(scenario))
|
|
83790
|
+
return false;
|
|
83426
83791
|
if (isFormValidationScenario(scenario))
|
|
83427
83792
|
return false;
|
|
83428
83793
|
const text = scenarioText(scenario);
|
|
@@ -83435,9 +83800,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83435
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)) {
|
|
83436
83801
|
return false;
|
|
83437
83802
|
}
|
|
83438
|
-
|
|
83439
|
-
|
|
83440
|
-
|
|
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;
|
|
83441
83811
|
}
|
|
83442
83812
|
function isNavigationChromeTap(action) {
|
|
83443
83813
|
if (action.type !== "TAP")
|
|
@@ -83526,12 +83896,46 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83526
83896
|
}
|
|
83527
83897
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
83528
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
|
+
}
|
|
83529
83913
|
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
83530
83914
|
return null;
|
|
83531
83915
|
}
|
|
83532
83916
|
if (isNavigationFocusedScenario(scenario)) {
|
|
83533
83917
|
return null;
|
|
83534
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
|
+
}
|
|
83535
83939
|
if (isFormValidationScenario(scenario)) {
|
|
83536
83940
|
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
83537
83941
|
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
@@ -83625,30 +84029,42 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83625
84029
|
}
|
|
83626
84030
|
}
|
|
83627
84031
|
const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
|
|
83628
|
-
if (
|
|
83629
|
-
|
|
83630
|
-
|
|
83631
|
-
|
|
83632
|
-
|
|
83633
|
-
|
|
83634
|
-
|
|
83635
|
-
|
|
83636
|
-
|
|
83637
|
-
|
|
83638
|
-
|
|
83639
|
-
|
|
83640
|
-
|
|
83641
|
-
|
|
83642
|
-
|
|
83643
|
-
|
|
83644
|
-
|
|
83645
|
-
|
|
83646
|
-
|
|
83647
|
-
|
|
83648
|
-
|
|
83649
|
-
|
|
83650
|
-
|
|
83651
|
-
|
|
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
|
+
}
|
|
83652
84068
|
}
|
|
83653
84069
|
return null;
|
|
83654
84070
|
}
|
|
@@ -83903,6 +84319,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83903
84319
|
lines.push("});");
|
|
83904
84320
|
return lines.join("\n");
|
|
83905
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
|
+
}
|
|
83906
84345
|
function evaluateMobileAssertionStrength(actions) {
|
|
83907
84346
|
const modelAsserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
|
|
83908
84347
|
if (modelAsserts.length === 0) {
|
|
@@ -83938,6 +84377,24 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83938
84377
|
}
|
|
83939
84378
|
};
|
|
83940
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
|
+
}
|
|
83941
84398
|
const code = mobileActionsToPseudoPlaywright(actions);
|
|
83942
84399
|
const interacted = actions.some((a) => a.type === "TAP" || a.type === "TYPE" || a.type === "CLEAR");
|
|
83943
84400
|
return (0, shared_1.validateAssertionStrength)(code, { interactionPerformed: interacted });
|
|
@@ -84241,7 +84698,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84241
84698
|
targetAppId,
|
|
84242
84699
|
credentials: credentialSecrets,
|
|
84243
84700
|
redactionCredentials: credentialValuesForRedaction,
|
|
84244
|
-
log: log2
|
|
84701
|
+
log: log2,
|
|
84702
|
+
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
|
|
84245
84703
|
});
|
|
84246
84704
|
consecutiveInfraFailures = 0;
|
|
84247
84705
|
const test = assembleTest({
|
|
@@ -84812,6 +85270,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
84812
85270
|
For a fix, return the COMPLETE steps array (not a diff).`;
|
|
84813
85271
|
var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
|
|
84814
85272
|
var RECOVERABLE_MAX = 3;
|
|
85273
|
+
var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
|
|
84815
85274
|
var BONUS_ATTEMPTS_CEIL = 4;
|
|
84816
85275
|
var DEFAULT_BONUS_ATTEMPTS = 2;
|
|
84817
85276
|
var MAX_LOG_CHARS = 4096;
|
|
@@ -85417,6 +85876,7 @@ ${args.directive}`);
|
|
|
85417
85876
|
const triedLocators = [];
|
|
85418
85877
|
let sameErrorCount = 0;
|
|
85419
85878
|
let lastErrorSignature = null;
|
|
85879
|
+
let consecutiveInvalidVerdicts = 0;
|
|
85420
85880
|
const markRecoverableTurn = () => {
|
|
85421
85881
|
if (recoverableUsed < RECOVERABLE_MAX) {
|
|
85422
85882
|
attemptCap += 1;
|
|
@@ -85490,11 +85950,22 @@ ${args.directive}`);
|
|
|
85490
85950
|
aiResponseCount += 1;
|
|
85491
85951
|
const parsed = parseHealVerdict(content);
|
|
85492
85952
|
if (!parsed) {
|
|
85493
|
-
|
|
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
|
+
}
|
|
85494
85964
|
markRecoverableTurn();
|
|
85495
|
-
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.";
|
|
85496
85966
|
continue;
|
|
85497
85967
|
}
|
|
85968
|
+
consecutiveInvalidVerdicts = 0;
|
|
85498
85969
|
if (TERMINAL_VERDICTS.has(parsed.verdict)) {
|
|
85499
85970
|
return { passed: false, verdict: parsed.verdict, healReason: parsed.reason, attemptsUsed: attempt };
|
|
85500
85971
|
}
|
|
@@ -337174,10 +337645,11 @@ Fatal error: ${msg}`);
|
|
|
337174
337645
|
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
337175
337646
|
if (shouldPreserveAppData) {
|
|
337176
337647
|
try {
|
|
337177
|
-
await driver.
|
|
337648
|
+
await driver.launchApp();
|
|
337649
|
+
log2("App activated with preserved data (no terminate/clear).");
|
|
337178
337650
|
} catch (err) {
|
|
337179
337651
|
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
|
|
337180
|
-
log2(`App
|
|
337652
|
+
log2(`App activate failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
|
|
337181
337653
|
}
|
|
337182
337654
|
return false;
|
|
337183
337655
|
}
|
|
@@ -352880,9 +353352,9 @@ var require_dist2 = __commonJS({
|
|
|
352880
353352
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
352881
353353
|
};
|
|
352882
353354
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
352883
|
-
exports.
|
|
352884
|
-
exports.
|
|
352885
|
-
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;
|
|
352886
353358
|
__exportStar(require_ai_provider(), exports);
|
|
352887
353359
|
__exportStar(require_mobile_types(), exports);
|
|
352888
353360
|
__exportStar(require_mobile_source_parser(), exports);
|
|
@@ -352907,6 +353379,18 @@ var require_dist2 = __commonJS({
|
|
|
352907
353379
|
Object.defineProperty(exports, "runMobileAIPlanner", { enumerable: true, get: function() {
|
|
352908
353380
|
return mobile_discovery_prompts_js_1.runMobileAIPlanner;
|
|
352909
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
|
+
} });
|
|
352910
353394
|
var mobile_generator_js_1 = require_mobile_generator();
|
|
352911
353395
|
Object.defineProperty(exports, "runMobileGeneratePhase", { enumerable: true, get: function() {
|
|
352912
353396
|
return mobile_generator_js_1.runMobileGeneratePhase;
|
|
@@ -353342,7 +353826,7 @@ var require_package4 = __commonJS({
|
|
|
353342
353826
|
"package.json"(exports, module) {
|
|
353343
353827
|
module.exports = {
|
|
353344
353828
|
name: "@validate.qa/runner",
|
|
353345
|
-
version: "1.0.
|
|
353829
|
+
version: "1.0.86",
|
|
353346
353830
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353347
353831
|
bin: {
|
|
353348
353832
|
"validate-runner": "dist/cli.js"
|
|
@@ -354208,9 +354692,9 @@ async function waitForColdBootSettle(sessionPath) {
|
|
|
354208
354692
|
timeoutMs: MOBILE_COLD_BOOT_SETTLE_MS,
|
|
354209
354693
|
minStableFrames: 2,
|
|
354210
354694
|
pollMs: 400,
|
|
354211
|
-
// WebView
|
|
354212
|
-
//
|
|
354213
|
-
minElements:
|
|
354695
|
+
// Native splash / WebView shell is ~5–8 nodes; RN product screens and rich
|
|
354696
|
+
// onboarding typically flatten to 15+ once JS has painted.
|
|
354697
|
+
minElements: 15
|
|
354214
354698
|
});
|
|
354215
354699
|
}
|
|
354216
354700
|
async function getWindowSize(sessionPath) {
|
|
@@ -354359,20 +354843,33 @@ async function sendKeysToFocusedInput(sessionPath, value) {
|
|
|
354359
354843
|
}
|
|
354360
354844
|
async function typeText(sessionPath, value, bounds) {
|
|
354361
354845
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
354846
|
+
if (!elementId && bounds === void 0) {
|
|
354847
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
354848
|
+
}
|
|
354362
354849
|
if (elementId) {
|
|
354363
354850
|
try {
|
|
354364
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/
|
|
354851
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
354365
354852
|
method: "POST",
|
|
354366
|
-
body: JSON.stringify({
|
|
354853
|
+
body: JSON.stringify({})
|
|
354367
354854
|
});
|
|
354368
|
-
return;
|
|
354369
354855
|
} catch (err) {
|
|
354370
354856
|
if (isAppiumDriverDeadError(err)) throw err;
|
|
354371
354857
|
}
|
|
354372
|
-
} else if (bounds === void 0) {
|
|
354373
|
-
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
354374
354858
|
}
|
|
354375
|
-
|
|
354859
|
+
try {
|
|
354860
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
354861
|
+
return;
|
|
354862
|
+
} catch (err) {
|
|
354863
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354864
|
+
}
|
|
354865
|
+
if (elementId) {
|
|
354866
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
354867
|
+
method: "POST",
|
|
354868
|
+
body: JSON.stringify({ text: value })
|
|
354869
|
+
});
|
|
354870
|
+
return;
|
|
354871
|
+
}
|
|
354872
|
+
throw new Error("TYPE failed: could not send keys to the focused input.");
|
|
354376
354873
|
}
|
|
354377
354874
|
async function submitKeyboard(sessionPath, bounds) {
|
|
354378
354875
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
@@ -355241,7 +355738,7 @@ async function pressHome(sessionPath, platform3) {
|
|
|
355241
355738
|
// src/services/device-quarantine.ts
|
|
355242
355739
|
var QUARANTINE_MAX_MS = (() => {
|
|
355243
355740
|
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
355244
|
-
return Number.isFinite(raw) && raw > 0 ? raw :
|
|
355741
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2 * 60 * 1e3;
|
|
355245
355742
|
})();
|
|
355246
355743
|
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
355247
355744
|
var nextQuarantineToken = 1;
|
|
@@ -356741,6 +357238,12 @@ async function createSession(target) {
|
|
|
356741
357238
|
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356742
357239
|
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356743
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,
|
|
356744
357247
|
...parallelCaps,
|
|
356745
357248
|
...realCaps
|
|
356746
357249
|
} : {
|
|
@@ -356752,6 +357255,12 @@ async function createSession(target) {
|
|
|
356752
357255
|
"appium:noReset": true,
|
|
356753
357256
|
// See the iOS branch: the 60s default kills AI-driven discovery sessions.
|
|
356754
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,
|
|
356755
357264
|
"appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
|
|
356756
357265
|
...parallelCaps,
|
|
356757
357266
|
...realCaps
|
|
@@ -359520,6 +360029,9 @@ async function startSession(validated, res) {
|
|
|
359520
360029
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
359521
360030
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
359522
360031
|
"appium:noReset": true,
|
|
360032
|
+
// Keep pace with executor createSession: Expo "Open in App?" sheets and
|
|
360033
|
+
// permission dialogs otherwise freeze WDA for recording/mirror sessions.
|
|
360034
|
+
...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
|
|
359523
360035
|
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
359524
360036
|
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
359525
360037
|
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
@@ -359527,6 +360039,12 @@ async function startSession(validated, res) {
|
|
|
359527
360039
|
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
359528
360040
|
// die with "Simulator.app does not exist" while executor sessions work.
|
|
359529
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,
|
|
359530
360048
|
...realCaps
|
|
359531
360049
|
} : {
|
|
359532
360050
|
platformName: "Android",
|
|
@@ -359535,6 +360053,10 @@ async function startSession(validated, res) {
|
|
|
359535
360053
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
359536
360054
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
359537
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,
|
|
359538
360060
|
"appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
|
|
359539
360061
|
...realCaps
|
|
359540
360062
|
};
|
|
@@ -360626,11 +361148,33 @@ var MobileBridgeDriver = class {
|
|
|
360626
361148
|
*/
|
|
360627
361149
|
async snapshot() {
|
|
360628
361150
|
return this.withSessionRecovery("snapshot", async () => {
|
|
360629
|
-
|
|
360630
|
-
|
|
360631
|
-
|
|
360632
|
-
|
|
360633
|
-
|
|
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
|
+
}
|
|
360634
361178
|
if (!xml && !screenshot && !windowSize) {
|
|
360635
361179
|
await this.assertSessionReachable();
|
|
360636
361180
|
}
|
|
@@ -363570,14 +364114,17 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363570
364114
|
// Android default) — a personal physical device must never have
|
|
363571
364115
|
// its app data wiped.
|
|
363572
364116
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
363573
|
-
|
|
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,
|
|
363574
364121
|
recoverSession: async (cause) => {
|
|
363575
364122
|
if (discoveryTeardownStarted) {
|
|
363576
364123
|
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
363577
364124
|
}
|
|
363578
364125
|
const lost = session;
|
|
363579
364126
|
mobileOnLog(
|
|
363580
|
-
`[appium] discovery session lost (${cause.message}); recreating
|
|
364127
|
+
`[appium] discovery session lost (${cause.message}); recreating on the leased device`
|
|
363581
364128
|
);
|
|
363582
364129
|
if (lost) {
|
|
363583
364130
|
detachMirrorSession(lost.deviceId);
|