@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.js
CHANGED
|
@@ -975,10 +975,11 @@ var require_mobile_source_parser = __commonJS({
|
|
|
975
975
|
function extractIOS(attrs) {
|
|
976
976
|
const role = nonEmpty3(attrs.type) ?? "XCUIElementTypeOther";
|
|
977
977
|
const name = nonEmpty3(attrs.name);
|
|
978
|
+
const identifier = nonEmpty3(attrs.identifier) ?? nonEmpty3(attrs.accessibilityIdentifier);
|
|
978
979
|
const labelAttr = nonEmpty3(attrs.label);
|
|
979
980
|
const value = nonEmpty3(attrs.value);
|
|
980
981
|
const label = name ?? labelAttr ?? value;
|
|
981
|
-
const accessibilityId = name;
|
|
982
|
+
const accessibilityId = identifier ?? name;
|
|
982
983
|
const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
|
|
983
984
|
const bounds = parseIOSBounds(attrs);
|
|
984
985
|
const enabled = attrBoolDefaultTrue(attrs.enabled);
|
|
@@ -1019,7 +1020,8 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1019
1020
|
const isGenericView = /android\.view\.View$/i.test(role);
|
|
1020
1021
|
const moderateHitTarget = hasArea && bounds.width >= 24 && bounds.height >= 24 && bounds.width <= 2e3 && bounds.height <= 1800;
|
|
1021
1022
|
const webHybridLabelledControl = isGenericView && hasHumanIdentity && moderateHitTarget && (focusable || clickable || longClickable || Boolean(contentDesc));
|
|
1022
|
-
const
|
|
1023
|
+
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);
|
|
1024
|
+
const actionable = !isAnrChrome && enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
|
|
1023
1025
|
return {
|
|
1024
1026
|
role,
|
|
1025
1027
|
label,
|
|
@@ -1197,6 +1199,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1197
1199
|
element.secure = fields.secure;
|
|
1198
1200
|
if (fields.enabled !== void 0)
|
|
1199
1201
|
element.enabled = fields.enabled;
|
|
1202
|
+
element.visible = fields.visible;
|
|
1200
1203
|
if (isCredibleStructuralNavigationTarget(element) && chainHasNativeNavigationSemantics(chain, platform3)) {
|
|
1201
1204
|
markNavigation(element, "native-navigation-semantics");
|
|
1202
1205
|
}
|
|
@@ -1571,6 +1574,8 @@ var require_mobile_boundary = __commonJS({
|
|
|
1571
1574
|
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports2) {
|
|
1572
1575
|
"use strict";
|
|
1573
1576
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1577
|
+
exports2.isAndroidAnrSystemResourceId = isAndroidAnrSystemResourceId;
|
|
1578
|
+
exports2.looksLikeAndroidAnrUi = looksLikeAndroidAnrUi;
|
|
1574
1579
|
exports2.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1575
1580
|
exports2.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1576
1581
|
exports2.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
@@ -1588,6 +1593,28 @@ var require_mobile_boundary = __commonJS({
|
|
|
1588
1593
|
"com.miui.securitycenter",
|
|
1589
1594
|
"com.samsung.android.permissioncontroller"
|
|
1590
1595
|
]);
|
|
1596
|
+
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;
|
|
1597
|
+
var ANDROID_ANR_TEXT_RE = /isn['’]?t responding|isn['’]?t responding\.|anr|close app|wait/i;
|
|
1598
|
+
function isAndroidAnrSystemResourceId(resourceId) {
|
|
1599
|
+
const rid = resourceId?.trim();
|
|
1600
|
+
if (!rid)
|
|
1601
|
+
return false;
|
|
1602
|
+
if (/(?:android:id\/)?aerr_/i.test(rid))
|
|
1603
|
+
return true;
|
|
1604
|
+
return ANDROID_ANR_RESOURCE_ID_RE.test(rid) && /aerr_/i.test(rid);
|
|
1605
|
+
}
|
|
1606
|
+
function looksLikeAndroidAnrUi(input) {
|
|
1607
|
+
if (isAndroidAnrSystemResourceId(input.resourceId))
|
|
1608
|
+
return true;
|
|
1609
|
+
const rid = input.resourceId?.trim() ?? "";
|
|
1610
|
+
const blob = [input.text, input.label, input.contentDesc].filter(Boolean).join(" ");
|
|
1611
|
+
if (/(?:android:id\/)?(?:alertTitle|aerr_)/i.test(rid) && ANDROID_ANR_TEXT_RE.test(blob)) {
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
if (/isn['’]?t responding/i.test(blob))
|
|
1615
|
+
return true;
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1591
1618
|
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1592
1619
|
"com.apple.springboard",
|
|
1593
1620
|
"com.apple.authenticationservicesui",
|
|
@@ -1745,7 +1772,7 @@ var init_constants = __esm({
|
|
|
1745
1772
|
MOBILE_APP_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,253}[A-Za-z0-9])?$/;
|
|
1746
1773
|
MOBILE_FOCUS_SETTLE_MS = 150;
|
|
1747
1774
|
MOBILE_POST_ACTION_SETTLE_MS = 1800;
|
|
1748
|
-
MOBILE_COLD_BOOT_SETTLE_MS =
|
|
1775
|
+
MOBILE_COLD_BOOT_SETTLE_MS = 25e3;
|
|
1749
1776
|
MOBILE_DEFAULT_SWIPE_DISTANCE = 500;
|
|
1750
1777
|
MOBILE_MAX_SWIPE_DISTANCE = 2e3;
|
|
1751
1778
|
SELECTOR_PRIORITIES = [
|
|
@@ -74887,7 +74914,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74887
74914
|
type: "function",
|
|
74888
74915
|
function: {
|
|
74889
74916
|
name: "mobile_relaunch",
|
|
74890
|
-
description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds).
|
|
74917
|
+
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.",
|
|
74891
74918
|
parameters: { type: "object", properties: {} }
|
|
74892
74919
|
}
|
|
74893
74920
|
};
|
|
@@ -75687,11 +75714,11 @@ Rules:
|
|
|
75687
75714
|
**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.
|
|
75688
75715
|
**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.
|
|
75689
75716
|
|
|
75690
|
-
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
|
|
75717
|
+
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.
|
|
75691
75718
|
|
|
75692
75719
|
## Boundaries (survey = map, do not mutate)
|
|
75693
75720
|
- Do NOT submit forms, save, delete, purchase, or send anything. You MAY open modals/menus to catalog them, then dismiss.
|
|
75694
|
-
- 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
|
|
75721
|
+
- 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).
|
|
75695
75722
|
- If you hit a login wall and have no credentials, capture the auth screen and explore what is reachable.
|
|
75696
75723
|
|
|
75697
75724
|
Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
@@ -75916,6 +75943,34 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75916
75943
|
latest = boundaryResolved;
|
|
75917
75944
|
lastSnapshotIteration = iterationForObservation;
|
|
75918
75945
|
if (boundary.kind === "transient_external") {
|
|
75946
|
+
const isHostHomeShell = /springboard|systemui/i.test(boundary.owner);
|
|
75947
|
+
if (isHostHomeShell) {
|
|
75948
|
+
log2(` Host shell detected from ${source}: ${boundary.owner}; activating ${boundary.targetAppId} so exploration can continue.`);
|
|
75949
|
+
try {
|
|
75950
|
+
await driver.launchApp();
|
|
75951
|
+
const snap = await driver.snapshot();
|
|
75952
|
+
const recovered = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
|
|
75953
|
+
if (recovered) {
|
|
75954
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
75955
|
+
if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
|
|
75956
|
+
const enriched = await enrichResolvedSnapshotWithHybridWeb(recovered, driver, credentialValuesForRedaction, log2, platform3, targetAppId, hybridContextCache, true, source !== "seed snapshot" && source !== "mobile_snapshot");
|
|
75957
|
+
latest = enriched;
|
|
75958
|
+
lastSnapshotIteration = iterationForObservation;
|
|
75959
|
+
const screenId = ingestSnapshot(enriched, iterationForObservation);
|
|
75960
|
+
captureScreenshot(enriched.replacementScreenshot ?? snap.screenshot, screenId);
|
|
75961
|
+
return {
|
|
75962
|
+
observation: enriched.observation,
|
|
75963
|
+
screenId,
|
|
75964
|
+
transientExternal: true,
|
|
75965
|
+
externalOwner: boundary.owner
|
|
75966
|
+
};
|
|
75967
|
+
}
|
|
75968
|
+
}
|
|
75969
|
+
} catch (err) {
|
|
75970
|
+
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
|
|
75971
|
+
log2(` Failed to recover from host shell ${boundary.owner}: ${err instanceof Error ? err.message : String(err)}`);
|
|
75972
|
+
}
|
|
75973
|
+
}
|
|
75919
75974
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
75920
75975
|
return {
|
|
75921
75976
|
observation: `${boundaryResolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
@@ -76102,6 +76157,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76102
76157
|
const kickoff = buildKickoffMessage(mode);
|
|
76103
76158
|
const recentExchanges = [];
|
|
76104
76159
|
const transientDirectives = [];
|
|
76160
|
+
const resetPolicyForTools = ctx.target?.resetPolicy ?? "AUTO";
|
|
76161
|
+
const preserveAppDataForTools = resetPolicyForTools === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicyForTools === "AUTO";
|
|
76105
76162
|
const tools = [
|
|
76106
76163
|
MOBILE_SNAPSHOT_TOOL,
|
|
76107
76164
|
MOBILE_TAP_TOOL,
|
|
@@ -76111,7 +76168,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76111
76168
|
MOBILE_SWIPE_TOOL,
|
|
76112
76169
|
MOBILE_BACK_TOOL,
|
|
76113
76170
|
MOBILE_HOME_TOOL,
|
|
76114
|
-
|
|
76171
|
+
// Omit relaunch under preserve — model otherwise cold-restarts RN into splash.
|
|
76172
|
+
...preserveAppDataForTools ? [] : [MOBILE_RELAUNCH_TOOL],
|
|
76115
76173
|
MOBILE_OPEN_DEEPLINK_TOOL,
|
|
76116
76174
|
CAPTURE_SCREEN_TOOL,
|
|
76117
76175
|
RECORD_TRANSITION_TOOL,
|
|
@@ -76130,6 +76188,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76130
76188
|
let summary = "";
|
|
76131
76189
|
let truncated = false;
|
|
76132
76190
|
let truncatedReason;
|
|
76191
|
+
const emptyUiTreeStreak = { count: 0 };
|
|
76133
76192
|
let finishRedirects = 0;
|
|
76134
76193
|
let lastFinishBreadthRemainingItems = null;
|
|
76135
76194
|
let lastFinishBreadthProgressMark = null;
|
|
@@ -76467,10 +76526,18 @@ Exploration complete: ${summary}`);
|
|
|
76467
76526
|
credentials: credentialSecrets,
|
|
76468
76527
|
redactionCredentials: credentialValuesForRedaction,
|
|
76469
76528
|
authTracker,
|
|
76470
|
-
inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null
|
|
76529
|
+
inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
|
|
76530
|
+
allowRelaunch: !preserveAppDataForTools,
|
|
76531
|
+
emptyUiTreeStreak
|
|
76471
76532
|
});
|
|
76472
76533
|
pushResult(handled);
|
|
76473
76534
|
const handledRecord = handled && typeof handled === "object" ? handled : null;
|
|
76535
|
+
if (handledRecord?.permanent === true && handledRecord?.emptyUiTreeStreak) {
|
|
76536
|
+
explorationComplete = true;
|
|
76537
|
+
summary = typeof handledRecord.reason === "string" && handledRecord.reason.trim() ? handledRecord.reason : "Accessibility tree remained empty after repeated Appium observations; exploration stopped early.";
|
|
76538
|
+
log2(`
|
|
76539
|
+
Stopping Phase 1 early: empty UI tree thrash (streak=${handledRecord.emptyUiTreeStreak}).`);
|
|
76540
|
+
}
|
|
76474
76541
|
const navigationWasAttempted = handledRecord?.ok !== false || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "screenId") || Object.prototype.hasOwnProperty.call(handledRecord ?? {}, "observation");
|
|
76475
76542
|
if (DEVICE_MUTATION_BARRIER_TOOLS.has(toolName) && navigationWasAttempted) {
|
|
76476
76543
|
navigationBarrierTool = toolName;
|
|
@@ -76559,14 +76626,24 @@ Exploration complete: ${summary}`);
|
|
|
76559
76626
|
}
|
|
76560
76627
|
return result;
|
|
76561
76628
|
}
|
|
76629
|
+
var MAX_EMPTY_UI_TREE_STREAK = 3;
|
|
76562
76630
|
async function dispatchMobileTool(args) {
|
|
76563
|
-
const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox } = args;
|
|
76631
|
+
const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox, allowRelaunch, emptyUiTreeStreak } = args;
|
|
76632
|
+
const noteEmptyUiTree = () => {
|
|
76633
|
+
emptyUiTreeStreak.count += 1;
|
|
76634
|
+
return emptyUiTreeStreak.count;
|
|
76635
|
+
};
|
|
76636
|
+
const clearEmptyUiTreeStreak = () => {
|
|
76637
|
+
emptyUiTreeStreak.count = 0;
|
|
76638
|
+
};
|
|
76564
76639
|
const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
|
|
76565
76640
|
(0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
|
|
76566
76641
|
const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
|
|
76567
76642
|
if (!resolved) {
|
|
76643
|
+
noteEmptyUiTree();
|
|
76568
76644
|
return Promise.resolve({ observation: "(no UI tree returned)", screenId: null });
|
|
76569
76645
|
}
|
|
76646
|
+
clearEmptyUiTreeStreak();
|
|
76570
76647
|
return absorbResolvedObservation(resolved, actionResult.screenshot, iteration, source, preferCredentialSecondary);
|
|
76571
76648
|
};
|
|
76572
76649
|
const currentAuthSurface = () => {
|
|
@@ -76596,8 +76673,17 @@ Exploration complete: ${summary}`);
|
|
|
76596
76673
|
const snap = await driver.snapshot();
|
|
76597
76674
|
const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
|
|
76598
76675
|
if (!resolved) {
|
|
76599
|
-
|
|
76676
|
+
const streak = noteEmptyUiTree();
|
|
76677
|
+
log2(` Snapshot returned no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
|
|
76678
|
+
return {
|
|
76679
|
+
ok: false,
|
|
76680
|
+
error: "snapshot returned no UI tree",
|
|
76681
|
+
emptyUiTreeStreak: streak,
|
|
76682
|
+
permanent: streak >= MAX_EMPTY_UI_TREE_STREAK,
|
|
76683
|
+
reason: streak >= MAX_EMPTY_UI_TREE_STREAK ? "Appium repeatedly returned an empty accessibility tree after settle/activate. Stop relaunch thrash \u2014 call finish_exploration with evidence you have, or abort. This is almost always a session/device/app-start failure, not missing product screens." : "No accessibility tree yet. Prefer one mobile_snapshot after a short wait over repeated relaunches."
|
|
76684
|
+
};
|
|
76600
76685
|
}
|
|
76686
|
+
clearEmptyUiTreeStreak();
|
|
76601
76687
|
const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_snapshot");
|
|
76602
76688
|
if (absorbed.screenId) {
|
|
76603
76689
|
const elementCount = state2.getScreen(absorbed.screenId)?.elements.length ?? resolved.screen.elements.length;
|
|
@@ -76963,14 +77049,37 @@ Exploration complete: ${summary}`);
|
|
|
76963
77049
|
}
|
|
76964
77050
|
// ----- Relaunch (hermetic reset) -----
|
|
76965
77051
|
case "mobile_relaunch": {
|
|
77052
|
+
if (!allowRelaunch) {
|
|
77053
|
+
return {
|
|
77054
|
+
ok: false,
|
|
77055
|
+
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.",
|
|
77056
|
+
note: "Relaunch refused under preserve policy."
|
|
77057
|
+
};
|
|
77058
|
+
}
|
|
77059
|
+
if (emptyUiTreeStreak.count >= MAX_EMPTY_UI_TREE_STREAK) {
|
|
77060
|
+
return {
|
|
77061
|
+
ok: false,
|
|
77062
|
+
error: `mobile_relaunch refused: accessibility tree has been empty for ${emptyUiTreeStreak.count} consecutive observations. Call finish_exploration or stop \u2014 further relaunches will not recover a dead session/app paint.`,
|
|
77063
|
+
permanent: true,
|
|
77064
|
+
emptyUiTreeStreak: emptyUiTreeStreak.count
|
|
77065
|
+
};
|
|
77066
|
+
}
|
|
76966
77067
|
authTracker?.noteInterveningMutation(iteration);
|
|
76967
77068
|
await driver.relaunch();
|
|
76968
77069
|
log2(" Relaunched app for hermetic reset.");
|
|
76969
77070
|
const snap = await driver.snapshot();
|
|
76970
77071
|
const resolved = buildResolved(snap.xml, snap.windowSize, snap.screenSignal);
|
|
76971
77072
|
if (!resolved) {
|
|
76972
|
-
|
|
77073
|
+
const streak = noteEmptyUiTree();
|
|
77074
|
+
log2(` Relaunch yielded no UI tree (empty streak ${streak}/${MAX_EMPTY_UI_TREE_STREAK}).`);
|
|
77075
|
+
return {
|
|
77076
|
+
ok: true,
|
|
77077
|
+
note: "relaunched; no UI tree returned \u2014 call mobile_snapshot.",
|
|
77078
|
+
emptyUiTreeStreak: streak,
|
|
77079
|
+
permanent: streak >= MAX_EMPTY_UI_TREE_STREAK
|
|
77080
|
+
};
|
|
76973
77081
|
}
|
|
77082
|
+
clearEmptyUiTreeStreak();
|
|
76974
77083
|
const absorbed = await absorbResolvedObservation(resolved, snap.screenshot, iteration, "mobile_relaunch");
|
|
76975
77084
|
return {
|
|
76976
77085
|
ok: true,
|
|
@@ -80202,6 +80311,10 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
80202
80311
|
"use strict";
|
|
80203
80312
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80204
80313
|
exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = void 0;
|
|
80314
|
+
exports2.isScrubbedPlaceholderName = isScrubbedPlaceholderName;
|
|
80315
|
+
exports2.isOpaqueMobileScreenName = isOpaqueMobileScreenName;
|
|
80316
|
+
exports2.isPackageLeafAppCatchAll = isPackageLeafAppCatchAll;
|
|
80317
|
+
exports2.isLowValueMobileAutoDeepFeature = isLowValueMobileAutoDeepFeature;
|
|
80205
80318
|
exports2.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
|
|
80206
80319
|
exports2.transitionSourceLabel = transitionSourceLabel;
|
|
80207
80320
|
exports2.buildMobileSiteMap = buildMobileSiteMap;
|
|
@@ -80255,7 +80368,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80255
80368
|
5. CRUD screens for the same entity belong together
|
|
80256
80369
|
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).
|
|
80257
80370
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
80258
|
-
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.
|
|
80371
|
+
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.
|
|
80259
80372
|
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.
|
|
80260
80373
|
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.
|
|
80261
80374
|
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.
|
|
@@ -80300,6 +80413,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80300
80413
|
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"]\`
|
|
80301
80414
|
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"]\`
|
|
80302
80415
|
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.
|
|
80416
|
+
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.
|
|
80303
80417
|
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.
|
|
80304
80418
|
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>."
|
|
80305
80419
|
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.
|
|
@@ -80454,10 +80568,26 @@ Each entry is exactly one of:
|
|
|
80454
80568
|
function screenDisplayName(state2) {
|
|
80455
80569
|
return state2.screenName?.trim() || state2.screenId;
|
|
80456
80570
|
}
|
|
80571
|
+
function isScrubbedPlaceholderName(value) {
|
|
80572
|
+
const trimmed = value.trim();
|
|
80573
|
+
if (!trimmed)
|
|
80574
|
+
return false;
|
|
80575
|
+
if (/^<[^<>\s]+>$/.test(trimmed))
|
|
80576
|
+
return true;
|
|
80577
|
+
if (/^(number|identity|email|id|url|token|value|secret|date|time)$/i.test(trimmed)) {
|
|
80578
|
+
return true;
|
|
80579
|
+
}
|
|
80580
|
+
const stripped = trimmed.replace(/<[^<>\s]+>/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
|
|
80581
|
+
if (!stripped)
|
|
80582
|
+
return true;
|
|
80583
|
+
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);
|
|
80584
|
+
}
|
|
80457
80585
|
function isOpaqueMobileScreenName(value, appId) {
|
|
80458
80586
|
const trimmed = value.trim();
|
|
80459
80587
|
if (!trimmed)
|
|
80460
80588
|
return false;
|
|
80589
|
+
if (isScrubbedPlaceholderName(trimmed))
|
|
80590
|
+
return true;
|
|
80461
80591
|
const lower = trimmed.toLowerCase();
|
|
80462
80592
|
const lowerAppId = appId?.trim().toLowerCase();
|
|
80463
80593
|
if (lowerAppId && (lower === lowerAppId || lower.startsWith(`${lowerAppId}#`) || lower.startsWith(`${lowerAppId}.`))) {
|
|
@@ -80465,6 +80595,32 @@ Each entry is exactly one of:
|
|
|
80465
80595
|
}
|
|
80466
80596
|
return /^[a-z][\w$]*(?:\.[a-z][\w$]*)+(?:#[a-z0-9_. -]+)?$/i.test(trimmed);
|
|
80467
80597
|
}
|
|
80598
|
+
function isPackageLeafAppCatchAll(name, appId) {
|
|
80599
|
+
const trimmed = name.trim();
|
|
80600
|
+
if (!trimmed || !appId?.trim())
|
|
80601
|
+
return false;
|
|
80602
|
+
const match = trimmed.match(/^(.+?)\s+app$/i);
|
|
80603
|
+
if (!match)
|
|
80604
|
+
return false;
|
|
80605
|
+
const brand = match[1].trim();
|
|
80606
|
+
if (!brand || /\s/.test(brand))
|
|
80607
|
+
return false;
|
|
80608
|
+
const leaf = appId.split(".").map((p) => p.trim()).filter(Boolean).pop();
|
|
80609
|
+
if (!leaf)
|
|
80610
|
+
return false;
|
|
80611
|
+
const normalize = (s) => s.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
80612
|
+
return normalize(brand) === normalize(leaf);
|
|
80613
|
+
}
|
|
80614
|
+
function isLowValueMobileAutoDeepFeature(name, appId) {
|
|
80615
|
+
const trimmed = name.trim();
|
|
80616
|
+
if (!trimmed)
|
|
80617
|
+
return true;
|
|
80618
|
+
if (isScrubbedPlaceholderName(trimmed) || isOpaqueMobileScreenName(trimmed, appId))
|
|
80619
|
+
return true;
|
|
80620
|
+
if (isPackageLeafAppCatchAll(trimmed, appId))
|
|
80621
|
+
return true;
|
|
80622
|
+
return false;
|
|
80623
|
+
}
|
|
80468
80624
|
function humanizeToken(token) {
|
|
80469
80625
|
const spaced = token.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
80470
80626
|
if (!spaced)
|
|
@@ -80479,11 +80635,12 @@ Each entry is exactly one of:
|
|
|
80479
80635
|
}
|
|
80480
80636
|
function nonGenericScreenName(state2, appId) {
|
|
80481
80637
|
const raw = state2.screenName?.trim();
|
|
80482
|
-
if (!raw || isOpaqueMobileScreenName(raw, appId))
|
|
80638
|
+
if (!raw || isOpaqueMobileScreenName(raw, appId) || isScrubbedPlaceholderName(raw))
|
|
80483
80639
|
return null;
|
|
80484
80640
|
const human = humanizeToken(raw.replace(/(?:Activity|ViewController|Fragment|Screen|Page)$/i, ""));
|
|
80485
|
-
if (!human || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human))
|
|
80641
|
+
if (!human || isScrubbedPlaceholderName(human) || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human)) {
|
|
80486
80642
|
return null;
|
|
80643
|
+
}
|
|
80487
80644
|
return human;
|
|
80488
80645
|
}
|
|
80489
80646
|
function inferMobileFeatureGroupName(states, appId) {
|
|
@@ -81212,6 +81369,45 @@ ${constraints.join("\n")}`);
|
|
|
81212
81369
|
return finalGroups;
|
|
81213
81370
|
}
|
|
81214
81371
|
var WEB_URL_PREFIX_FALLBACK_LOG_RE = /\bFallback:\s*\d+\s+feature groups? from URL-prefix grouping\b/i;
|
|
81372
|
+
function graphHasAuthenticatedProduct(graph) {
|
|
81373
|
+
for (const state2 of graph.screens.values()) {
|
|
81374
|
+
if (state2.requiresAuth === true)
|
|
81375
|
+
return true;
|
|
81376
|
+
if (state2.authProvenance === "post-submit-authenticated" || state2.authProvenance === "preserved-session-product") {
|
|
81377
|
+
return true;
|
|
81378
|
+
}
|
|
81379
|
+
}
|
|
81380
|
+
return false;
|
|
81381
|
+
}
|
|
81382
|
+
function isGuestWallScenario(scenario) {
|
|
81383
|
+
const feature = (scenario.featureGroup ?? "").toLowerCase();
|
|
81384
|
+
if (/auth|sign[\s-]?in|log[\s-]?in|onboarding/.test(feature))
|
|
81385
|
+
return false;
|
|
81386
|
+
const stepText = (scenario.steps ?? []).map((step) => typeof step === "string" ? step : step.action ?? "").join(" ");
|
|
81387
|
+
const outcomeText = (scenario.expectedOutcomes ?? []).map((outcome) => outcome.description ?? "").join(" ");
|
|
81388
|
+
const blob = [scenario.name, scenario.goal, stepText, outcomeText].filter(Boolean).join("\n").toLowerCase();
|
|
81389
|
+
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)) {
|
|
81390
|
+
return true;
|
|
81391
|
+
}
|
|
81392
|
+
const productAction = /\b(like|unlike|download|playlist|comment|follow|share|clip|save|favorite|bookmark|subscribe|purchase|buy|checkout)\b/i.test(blob);
|
|
81393
|
+
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);
|
|
81394
|
+
return productAction && assertsLoginGate;
|
|
81395
|
+
}
|
|
81396
|
+
function filterGuestWallScenariosForAuthSession(scenarios, graph) {
|
|
81397
|
+
if (scenarios.length === 0 || !graphHasAuthenticatedProduct(graph)) {
|
|
81398
|
+
return { scenarios, dropped: 0 };
|
|
81399
|
+
}
|
|
81400
|
+
const kept = [];
|
|
81401
|
+
let dropped = 0;
|
|
81402
|
+
for (const scenario of scenarios) {
|
|
81403
|
+
if (isGuestWallScenario(scenario)) {
|
|
81404
|
+
dropped += 1;
|
|
81405
|
+
continue;
|
|
81406
|
+
}
|
|
81407
|
+
kept.push(scenario);
|
|
81408
|
+
}
|
|
81409
|
+
return { scenarios: kept, dropped };
|
|
81410
|
+
}
|
|
81215
81411
|
function selectMobilePlannerFeatureGroups(result, graph, appId, mode, existingFeatureNames = []) {
|
|
81216
81412
|
if (mode !== "survey") {
|
|
81217
81413
|
return { accepted: result.featureGroups, rejected: [] };
|
|
@@ -81325,7 +81521,12 @@ ${constraints.join("\n")}`);
|
|
|
81325
81521
|
}
|
|
81326
81522
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(plannerGroupsForFiltering, siteMap, input.graph, input.appId, input.mode === "survey");
|
|
81327
81523
|
const mobileFeatureGroups = plannerFeatureGroups.length > 0 ? plannerFeatureGroups : buildMobileFeatureGroupsFromGraph(input.graph, input.appId);
|
|
81328
|
-
|
|
81524
|
+
const guestWallFilter = input.mode !== "survey" ? filterGuestWallScenariosForAuthSession(result.scenarios, input.graph) : { scenarios: result.scenarios, dropped: 0 };
|
|
81525
|
+
if (guestWallFilter.dropped > 0) {
|
|
81526
|
+
mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
|
|
81527
|
+
}
|
|
81528
|
+
const plannedScenarios = guestWallFilter.scenarios;
|
|
81529
|
+
if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
|
|
81329
81530
|
const fallbackScenarios = buildMobileFallbackScenarios(input);
|
|
81330
81531
|
if (fallbackScenarios.length > 0) {
|
|
81331
81532
|
return {
|
|
@@ -81344,6 +81545,7 @@ ${constraints.join("\n")}`);
|
|
|
81344
81545
|
if (input.featureName?.trim()) {
|
|
81345
81546
|
return {
|
|
81346
81547
|
...result,
|
|
81548
|
+
scenarios: plannedScenarios,
|
|
81347
81549
|
featureGroups: mobileFeatureGroups,
|
|
81348
81550
|
logs: [...result.logs, ...mobileAdaptationLogs],
|
|
81349
81551
|
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(" | "),
|
|
@@ -81380,6 +81582,7 @@ ${constraints.join("\n")}`);
|
|
|
81380
81582
|
}
|
|
81381
81583
|
return {
|
|
81382
81584
|
...result,
|
|
81585
|
+
scenarios: plannedScenarios,
|
|
81383
81586
|
featureGroups: mobileFeatureGroups,
|
|
81384
81587
|
logs: [...result.logs, ...mobileAdaptationLogs]
|
|
81385
81588
|
};
|
|
@@ -81449,6 +81652,7 @@ var require_mobile_generator = __commonJS({
|
|
|
81449
81652
|
exports2.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81450
81653
|
exports2.elideNoOpDetours = elideNoOpDetours;
|
|
81451
81654
|
exports2.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81655
|
+
exports2.isEphemeralContentAssertText = isEphemeralContentAssertText;
|
|
81452
81656
|
exports2.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81453
81657
|
exports2.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
81454
81658
|
exports2.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
@@ -81909,15 +82113,20 @@ ${hybridSkeleton.join("\n")}`;
|
|
|
81909
82113
|
- Every element is addressed by a ref like e1, e2. Refs are ONLY valid for the most recent mobile_snapshot. Re-snapshot after every action that changes the screen, then use the NEW refs.
|
|
81910
82114
|
- mobile_tap/mobile_type/mobile_clear take a ref; the loop resolves it to coordinates AND records the durable locator for replay \u2014 you never pass coordinates.
|
|
81911
82115
|
|
|
81912
|
-
## Your job
|
|
82116
|
+
## Your job \u2014 feature fidelity (non-negotiable)
|
|
82117
|
+
The scenario name + goal + steps describe ONE product behavior from survey/discovery. The generated test MUST implement that behavior on THIS app \u2014 not a random walk through tabs.
|
|
82118
|
+
|
|
81913
82119
|
1. mobile_snapshot the current screen.
|
|
81914
|
-
2. Execute the scenario's steps using the real UI: tap
|
|
81915
|
-
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way
|
|
81916
|
-
|
|
81917
|
-
-
|
|
81918
|
-
-
|
|
81919
|
-
|
|
81920
|
-
|
|
82120
|
+
2. Execute the scenario's steps using the real UI: tap the controls that perform the goal, fill fields (mobile_type), submit, navigate to the destination named in the goal.
|
|
82121
|
+
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way:
|
|
82122
|
+
- Form scenarios (sign in, sign up, search, create, change password, edit profile): you MUST commit \u2014 tap submit/primary OR mobile_submit_keyboard. Filling alone is NOT enough. Opening a form without typing is NOT enough for mutation goals.
|
|
82123
|
+
- Content actions (like, share, follow, bookmark, download, \u2026): you MUST tap the control that performs that action \u2014 tab thrash / swipes alone are WRONG.
|
|
82124
|
+
- Navigation scenarios: you MUST land on the destination and assert something unique to it.
|
|
82125
|
+
4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
|
|
82126
|
+
- mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
|
|
82127
|
+
- After sign-in \u2192 signed-in product signal; after like \u2192 liked/unlike control or durable like state; after open Settings \u2192 a settings-specific control.
|
|
82128
|
+
- Do NOT assert bottom-nav tab labels alone, notification badge counts ("N unread"), or one-time empty-state CTAs as the scenario outcome.
|
|
82129
|
+
5. Call finish_generate with verified:true ONLY if you completed the goal AND asserted a matching durable outcome. If the app cannot complete the goal, finish with verified:false and explain \u2014 never invent a green path from unrelated screens.
|
|
81921
82130
|
|
|
81922
82131
|
## Rules
|
|
81923
82132
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
@@ -81968,7 +82177,7 @@ ${stepLines || " (no explicit steps \u2014 explore the goal directly)"}
|
|
|
81968
82177
|
Expected outcomes to PROVE with assert tools:
|
|
81969
82178
|
${outcomeLines || " - The target screen is reached and a stable element is visible."}
|
|
81970
82179
|
|
|
81971
|
-
Start by calling mobile_snapshot, then execute the steps
|
|
82180
|
+
Start by calling mobile_snapshot, then execute ONLY the steps needed for this goal \u2014 no tab thrash, no unrelated screens. Every recorded action becomes a permanent test step. Finish with at least one assert that proves THIS goal's outcome (from Expected outcomes), then finish_generate. If you cannot perform the goal on this app build, finish_generate with verified:false rather than a fake green path.`;
|
|
81972
82181
|
}
|
|
81973
82182
|
function buildStatePacket(input) {
|
|
81974
82183
|
const { scenario, iteration, maxIterations, latestObservation, assertCount, actionCount } = input;
|
|
@@ -82025,7 +82234,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
82025
82234
|
}
|
|
82026
82235
|
}
|
|
82027
82236
|
async function runScenarioLoop(args) {
|
|
82028
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog } = args;
|
|
82237
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
|
|
82029
82238
|
const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
|
|
82030
82239
|
const actions = [];
|
|
82031
82240
|
let assertCount = 0;
|
|
@@ -82599,6 +82808,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82599
82808
|
setLatest,
|
|
82600
82809
|
credentials,
|
|
82601
82810
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82811
|
+
allowRelaunch,
|
|
82602
82812
|
recordAction: (action) => {
|
|
82603
82813
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82604
82814
|
const structuralKey = structuralScreenKey(getLatest());
|
|
@@ -82633,7 +82843,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82633
82843
|
return { actions, assertCount, finish, lastSnapshot: lastTargetSnapshot, lastScreenName: lastTargetScreenName };
|
|
82634
82844
|
}
|
|
82635
82845
|
async function dispatchGenerateTool(args) {
|
|
82636
|
-
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
82846
|
+
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, log: log2 } = args;
|
|
82637
82847
|
const unknownRef = (ref) => ({
|
|
82638
82848
|
ok: false,
|
|
82639
82849
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -83023,6 +83233,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83023
83233
|
return actionPayload(result, absorbed);
|
|
83024
83234
|
}
|
|
83025
83235
|
case "mobile_relaunch": {
|
|
83236
|
+
if (!allowRelaunch) {
|
|
83237
|
+
log2(" relaunch refused under PRESERVE_APP_DATA \u2014 staying on current app instance.");
|
|
83238
|
+
const snap2 = await driver.snapshot();
|
|
83239
|
+
const absorbed2 = await absorbObservation(snap2.xml, snap2.screenSignal, snap2.windowSize, snap2.screenshot, "mobile_relaunch_blocked");
|
|
83240
|
+
return {
|
|
83241
|
+
ok: false,
|
|
83242
|
+
error: "mobile_relaunch is blocked while app data is preserved. Continue from the current screen.",
|
|
83243
|
+
observation: absorbed2.observation
|
|
83244
|
+
};
|
|
83245
|
+
}
|
|
83026
83246
|
await driver.relaunch();
|
|
83027
83247
|
const snap = await driver.snapshot();
|
|
83028
83248
|
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
@@ -83241,14 +83461,48 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83241
83461
|
});
|
|
83242
83462
|
return { ok: true, label: metadata.targetLabel ?? accessibilityId ?? text ?? "element" };
|
|
83243
83463
|
}
|
|
83464
|
+
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;
|
|
83465
|
+
function isDisplayableLandingCandidate(el, windowSize) {
|
|
83466
|
+
if (el.visible === false)
|
|
83467
|
+
return false;
|
|
83468
|
+
const { x, y, width, height } = el.bounds;
|
|
83469
|
+
if (!(width > 0 && height > 0))
|
|
83470
|
+
return false;
|
|
83471
|
+
if (width < 8 || height < 8)
|
|
83472
|
+
return false;
|
|
83473
|
+
if (windowSize && windowSize.width > 0 && windowSize.height > 0) {
|
|
83474
|
+
const centerX = x + width / 2;
|
|
83475
|
+
const centerY = y + height / 2;
|
|
83476
|
+
if (centerX < 0 || centerY < 0 || centerX > windowSize.width || centerY > windowSize.height) {
|
|
83477
|
+
return false;
|
|
83478
|
+
}
|
|
83479
|
+
if (width >= windowSize.width * 0.92 && height >= windowSize.height * 0.55) {
|
|
83480
|
+
return false;
|
|
83481
|
+
}
|
|
83482
|
+
}
|
|
83483
|
+
return true;
|
|
83484
|
+
}
|
|
83485
|
+
function landingAnchorSortKey(el) {
|
|
83486
|
+
const label = (nonEmpty3(el.label) || nonEmpty3(el.text) || "").trim();
|
|
83487
|
+
const ambient = AMBIENT_CHROME_LABEL_RE.test(label) ? 1 : 0;
|
|
83488
|
+
const labelled = nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
|
|
83489
|
+
const area = Math.max(1, el.bounds.width * el.bounds.height);
|
|
83490
|
+
const areaBucket = area < 8e4 ? 0 : area < 25e4 ? 1 : 2;
|
|
83491
|
+
return [ambient, labelled, areaBucket];
|
|
83492
|
+
}
|
|
83244
83493
|
function pickLandingAnchor(snapshot) {
|
|
83245
83494
|
if (!snapshot || snapshot.screen.elements.length === 0)
|
|
83246
83495
|
return null;
|
|
83247
83496
|
const elements = snapshot.screen.elements;
|
|
83248
|
-
const
|
|
83249
|
-
stableAnchors.sort((a, b) => {
|
|
83250
|
-
const
|
|
83251
|
-
|
|
83497
|
+
const windowSize = snapshot.screen.windowSize ?? null;
|
|
83498
|
+
const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)).sort((a, b) => {
|
|
83499
|
+
const ka = landingAnchorSortKey(a);
|
|
83500
|
+
const kb = landingAnchorSortKey(b);
|
|
83501
|
+
for (let i = 0; i < ka.length; i += 1) {
|
|
83502
|
+
if (ka[i] !== kb[i])
|
|
83503
|
+
return ka[i] - kb[i];
|
|
83504
|
+
}
|
|
83505
|
+
return 0;
|
|
83252
83506
|
});
|
|
83253
83507
|
const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83254
83508
|
if (contentfulNonButton)
|
|
@@ -83258,8 +83512,10 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83258
83512
|
return nonButton;
|
|
83259
83513
|
if (stableAnchors.length > 0)
|
|
83260
83514
|
return stableAnchors[0];
|
|
83261
|
-
const labelled = elements.find((el) => !weakAssertionTargetReason(el) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83262
|
-
|
|
83515
|
+
const labelled = elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
|
|
83516
|
+
if (labelled)
|
|
83517
|
+
return labelled;
|
|
83518
|
+
return elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)) ?? null;
|
|
83263
83519
|
}
|
|
83264
83520
|
var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
|
|
83265
83521
|
var ALWAYS_PRESENT_CONTAINER_ID_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -83357,10 +83613,19 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83357
83613
|
const anchor = pickLandingAnchor(recording.lastSnapshot);
|
|
83358
83614
|
if (!anchor)
|
|
83359
83615
|
return false;
|
|
83616
|
+
const modelAssertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83617
|
+
const anchorLabel = (nonEmpty3(anchor.label) || nonEmpty3(anchor.text) || "").trim();
|
|
83618
|
+
if (modelAssertCount > 0 && AMBIENT_CHROME_LABEL_RE.test(anchorLabel)) {
|
|
83619
|
+
return false;
|
|
83620
|
+
}
|
|
83621
|
+
const anchorSelector = (0, mobile_source_parser_js_2.preferredLocator)(anchor);
|
|
83622
|
+
const alreadyAsserted = actions.some((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") && typeof a.selector === "string" && a.selector === anchorSelector);
|
|
83623
|
+
if (alreadyAsserted)
|
|
83624
|
+
return false;
|
|
83360
83625
|
const screen = recording.lastScreenName || "LandingScreen";
|
|
83361
83626
|
actions.push(finalizeAction({
|
|
83362
83627
|
type: "ASSERT_VISIBLE",
|
|
83363
|
-
selector:
|
|
83628
|
+
selector: anchorSelector,
|
|
83364
83629
|
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83365
83630
|
}));
|
|
83366
83631
|
return true;
|
|
@@ -83379,6 +83644,84 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83379
83644
|
"element-hidden",
|
|
83380
83645
|
"screen-visible"
|
|
83381
83646
|
]);
|
|
83647
|
+
var CONTENT_ACTION_VERB_RE = /\b(like|unlike|favorite|favourite|unfavorite|unfavourite|heart|unheart|follow|unfollow|share|repost|retweet|bookmark|unbookmark|save\s+to|download|subscribe|unsubscribe|block|report|unpin|pin\s+(?:post|item|message|tweet)|mute|unmute|react|upvote|downvote|clap|boost)\b/i;
|
|
83648
|
+
var NAV_CHROME_TAP_RE = /\b(home|you|profile|search|settings|notifications?|inbox|messages?|explore|library|more|menu|tab|bottom\s*nav|videos?|create|feed)\b/i;
|
|
83649
|
+
var VOLATILE_BADGE_ASSERT_RE = /\b\d+\s*(unread|notifications?|messages?|likes?|followers?|comments?|views?)\b|\bunread\b.*\b\d+\b/i;
|
|
83650
|
+
function isContentActionScenario(scenario) {
|
|
83651
|
+
const text = scenarioText(scenario);
|
|
83652
|
+
if (!CONTENT_ACTION_VERB_RE.test(text))
|
|
83653
|
+
return false;
|
|
83654
|
+
if (/\b(view|see|show|open|browse|list|count)\s+(the\s+)?(likes?|followers?|shares?|bookmarks?)\b/i.test(text)) {
|
|
83655
|
+
return false;
|
|
83656
|
+
}
|
|
83657
|
+
return true;
|
|
83658
|
+
}
|
|
83659
|
+
function isContentActionTap(action) {
|
|
83660
|
+
if (action.type !== "TAP")
|
|
83661
|
+
return false;
|
|
83662
|
+
const textForAction = actionText(action);
|
|
83663
|
+
if (NAV_CHROME_TAP_RE.test(textForAction) && !CONTENT_ACTION_VERB_RE.test(textForAction)) {
|
|
83664
|
+
return false;
|
|
83665
|
+
}
|
|
83666
|
+
return CONTENT_ACTION_VERB_RE.test(textForAction);
|
|
83667
|
+
}
|
|
83668
|
+
function isProductiveScenarioAction(action, scenarioTextLower = "") {
|
|
83669
|
+
if (action.type === "TYPE" || action.type === "CLEAR")
|
|
83670
|
+
return true;
|
|
83671
|
+
if (action.type === "TAP") {
|
|
83672
|
+
const t = actionText(action);
|
|
83673
|
+
if (CONTENT_ACTION_VERB_RE.test(t) || isFormCommitAction(action) || isCredentialSubmitTap(action)) {
|
|
83674
|
+
return true;
|
|
83675
|
+
}
|
|
83676
|
+
if (NAV_CHROME_TAP_RE.test(t)) {
|
|
83677
|
+
if (!scenarioTextLower)
|
|
83678
|
+
return false;
|
|
83679
|
+
const chromeWords = t.match(/\b(home|you|profile|search|settings|notifications?|inbox|messages?|explore|library|more|menu|videos?|create|feed)\b/gi) ?? [];
|
|
83680
|
+
return chromeWords.some((w) => scenarioTextLower.includes(w.toLowerCase()));
|
|
83681
|
+
}
|
|
83682
|
+
return true;
|
|
83683
|
+
}
|
|
83684
|
+
return false;
|
|
83685
|
+
}
|
|
83686
|
+
function isNavChromeThrashTrace(actions) {
|
|
83687
|
+
const meaningful = actions.filter((a) => a.type !== "WAIT_FOR_ELEMENT");
|
|
83688
|
+
if (meaningful.length === 0)
|
|
83689
|
+
return false;
|
|
83690
|
+
const hasNavOrGesture = meaningful.some((a) => a.type === "TAP" || a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME");
|
|
83691
|
+
if (!hasNavOrGesture)
|
|
83692
|
+
return false;
|
|
83693
|
+
return meaningful.every((a) => {
|
|
83694
|
+
if (a.type === "SWIPE" || a.type === "BACK" || a.type === "HOME")
|
|
83695
|
+
return true;
|
|
83696
|
+
if (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") {
|
|
83697
|
+
const t = actionText(a);
|
|
83698
|
+
return NAV_CHROME_TAP_RE.test(t) || VOLATILE_BADGE_ASSERT_RE.test(t) || isStructuralOnlyAssert(a);
|
|
83699
|
+
}
|
|
83700
|
+
if (a.type === "TAP") {
|
|
83701
|
+
return NAV_CHROME_TAP_RE.test(actionText(a)) && !CONTENT_ACTION_VERB_RE.test(actionText(a));
|
|
83702
|
+
}
|
|
83703
|
+
return false;
|
|
83704
|
+
});
|
|
83705
|
+
}
|
|
83706
|
+
function isHarnessOrVisibilityScenario(scenario) {
|
|
83707
|
+
const text = scenarioText(scenario);
|
|
83708
|
+
return /\b(avoid|recover|guard|remain|stay\s+in|does\s+not\s+record|external|outside\s+the\s+app|inspect|visibility\s+proof|landing\s+screen|smoke|structural\s+assert|skip\s+link|global\s+navigation\s+anchors?)\b/i.test(text);
|
|
83709
|
+
}
|
|
83710
|
+
function requiresFeatureProgress(scenario) {
|
|
83711
|
+
if (isNavigationFocusedScenario(scenario))
|
|
83712
|
+
return false;
|
|
83713
|
+
if (isCancelDiscardScenario(scenario))
|
|
83714
|
+
return false;
|
|
83715
|
+
if (isContentActionScenario(scenario))
|
|
83716
|
+
return true;
|
|
83717
|
+
if (isHarnessOrVisibilityScenario(scenario))
|
|
83718
|
+
return false;
|
|
83719
|
+
const text = scenarioText(scenario);
|
|
83720
|
+
const behaviorClaim = scenario.type === "HAPPY_PATH" || scenario.type === "USER_JOURNEY" || /\b(happy\s*path|successful|success|complete|user\s+can|should\s+be\s+able)\b/i.test(text);
|
|
83721
|
+
if (!behaviorClaim)
|
|
83722
|
+
return false;
|
|
83723
|
+
return /\b(like|unlike|share|follow|bookmark|download|subscribe|create|save|send|submit|edit|delete|remove|add|play|search|type|fill|login|sign\s*in|log\s*in|register|checkout|update|change|open|view|browse|filter|sort|upload|invite|join)\b/i.test(text);
|
|
83724
|
+
}
|
|
83382
83725
|
function isInspectionOnlyScenario(scenario, actions) {
|
|
83383
83726
|
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
83384
83727
|
return false;
|
|
@@ -83388,16 +83731,25 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83388
83731
|
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
83389
83732
|
return false;
|
|
83390
83733
|
}
|
|
83734
|
+
if (isContentActionScenario(scenario)) {
|
|
83735
|
+
return false;
|
|
83736
|
+
}
|
|
83391
83737
|
const text = scenarioText(scenario);
|
|
83392
|
-
const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|
|
|
83738
|
+
const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|provider\s+buttons?|sign[- ]?in\s+options|auth(?:entication)?\s+options|available\s+options)\b/.test(text) || /\b(show|display)\s+(all\s+)?(the\s+)?(providers?|options|buttons?|links?|entry\s+points?)\b/.test(text) || /\b(all\s+(three\s+)?providers?)\b/.test(text)) && !/\b(change|update|create|save|send|register|edit|with\s+valid|successful\s+(sign|log)|complete\s+(the\s+)?(login|sign[- ]?in)|submit\s+(the\s+)?(form|credentials)|enter\s+(an?\s+)?(email|password))\b/.test(text);
|
|
83393
83739
|
if (visibilityOnlyGoal) {
|
|
83394
83740
|
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83395
83741
|
return assertCount2 > 0;
|
|
83396
83742
|
}
|
|
83397
|
-
const completionIntent = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase))\b/.test(text);
|
|
83743
|
+
const completionIntent = scenario.type === "HAPPY_PATH" || scenario.type === "USER_JOURNEY" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase)|user\s+can|should\s+be\s+able)\b/.test(text);
|
|
83398
83744
|
const formIntent = /\b(sign\s*in|log\s*in|login|authenticate|register|sign\s*up|checkout|create|save|submit|send|change|update|edit)\b/.test(text);
|
|
83399
83745
|
if (completionIntent && formIntent)
|
|
83400
83746
|
return false;
|
|
83747
|
+
if (completionIntent && /\b(like|unlike|share|follow|bookmark|download|subscribe|create|save|send|post|delete|remove|add|play|pause)\b/i.test(text)) {
|
|
83748
|
+
return false;
|
|
83749
|
+
}
|
|
83750
|
+
if (completionIntent) {
|
|
83751
|
+
return false;
|
|
83752
|
+
}
|
|
83401
83753
|
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
83402
83754
|
return assertCount > 0;
|
|
83403
83755
|
}
|
|
@@ -83407,7 +83759,18 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83407
83759
|
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
83408
83760
|
return navigates && !completesFlow;
|
|
83409
83761
|
}
|
|
83762
|
+
function isCancelDiscardScenario(scenario) {
|
|
83763
|
+
const text = scenarioText(scenario);
|
|
83764
|
+
if (/\bcancel\s+(?:the|my|a|an|this|your|their)?\s*(subscription|order|booking|trip|reservation|membership|plan|purchase|payment|appointment|ticket|flight|ride)\b/i.test(text)) {
|
|
83765
|
+
return false;
|
|
83766
|
+
}
|
|
83767
|
+
const cancelEditPhrase = /\b(cancel|discard|revert|undo)\s+(?:(?:the|my|a|an|this|your|their|all)\s+)?(?:unsaved\s+)?(change|changes|edit|edits|update|updates|draft|input|inputs)\b/i.test(text) || /\b(change|changes|edit|edits|update|updates|draft)\s+(?:are\s+)?(cancelled|canceled|discarded|reverted|undone)\b/i.test(text) || /\breject\s+(?:(?:the|my|a|an|this|your|their|all)\s+)?(change|changes|edit|edits|update|updates|draft)\b/i.test(text);
|
|
83768
|
+
const discardWithoutSave = /\b(without\s+saving|do\s+not\s+save|don't\s+save)\b/i.test(text) && /\b(cancel|discard|revert|undo|leave|exit|back|dismiss)\b/i.test(text);
|
|
83769
|
+
return cancelEditPhrase || discardWithoutSave;
|
|
83770
|
+
}
|
|
83410
83771
|
function isFormValidationScenario(scenario) {
|
|
83772
|
+
if (isCancelDiscardScenario(scenario))
|
|
83773
|
+
return false;
|
|
83411
83774
|
if (scenario.variant === "validation")
|
|
83412
83775
|
return true;
|
|
83413
83776
|
if (scenario.type !== "EDGE_CASE")
|
|
@@ -83418,6 +83781,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83418
83781
|
function isFormMutationHappyPath(scenario) {
|
|
83419
83782
|
if (isNavigationFocusedScenario(scenario))
|
|
83420
83783
|
return false;
|
|
83784
|
+
if (isCancelDiscardScenario(scenario))
|
|
83785
|
+
return false;
|
|
83421
83786
|
if (isFormValidationScenario(scenario))
|
|
83422
83787
|
return false;
|
|
83423
83788
|
const text = scenarioText(scenario);
|
|
@@ -83430,9 +83795,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83430
83795
|
if (/\b(show|display|visible|options?|providers?|links?)\b/.test(text) && !/\b(change|update|create|save|send|register|edit|post|write|compose|delete|add|set)\b/.test(text)) {
|
|
83431
83796
|
return false;
|
|
83432
83797
|
}
|
|
83433
|
-
|
|
83434
|
-
|
|
83435
|
-
|
|
83798
|
+
if (/\b(play|pause|resume|playback|seek|volume|mute|unmute|shuffle|repeat|now\s+playing|mini\s*player)\b/.test(text) && !/\b(playlist\s+name|edit\s+(track|song|clip)|upload|caption|comment|description|save\s+to|add\s+to\s+(a\s+)?playlist|create\s+(a\s+)?playlist|rename)\b/.test(text)) {
|
|
83799
|
+
return false;
|
|
83800
|
+
}
|
|
83801
|
+
const formObject = "password|email|profile|message|post|item|record|form|settings|account|name|title|description|comment|bio|phone|address|payment|card|user|note|task|entry";
|
|
83802
|
+
const mutationVerb = "change|update|create|save|send|register|sign\\s*up|checkout|edit|post|write|compose|set|add|delete|remove";
|
|
83803
|
+
const determiner = "(?:(?:the|a|an|my|your|their|this|that)\\s+)?(?:new\\s+)?";
|
|
83804
|
+
const mutatesFormObject = new RegExp(`\\b(?:${mutationVerb})\\s+${determiner}(?:${formObject})\\b`).test(text) || new RegExp(`\\b(?:${formObject})\\s+(?:${mutationVerb})\\b`).test(text);
|
|
83805
|
+
return mutatesFormObject;
|
|
83436
83806
|
}
|
|
83437
83807
|
function isNavigationChromeTap(action) {
|
|
83438
83808
|
if (action.type !== "TAP")
|
|
@@ -83521,12 +83891,46 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83521
83891
|
}
|
|
83522
83892
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
83523
83893
|
const text = scenarioText(scenario);
|
|
83894
|
+
if (isContentActionScenario(scenario)) {
|
|
83895
|
+
const hasActionTap = actions.some((a) => isContentActionTap(a));
|
|
83896
|
+
if (!hasActionTap) {
|
|
83897
|
+
return "content-action scenario (like/share/follow/\u2026) did not record a tap on a matching control";
|
|
83898
|
+
}
|
|
83899
|
+
if (isNavChromeThrashTrace(actions)) {
|
|
83900
|
+
return "content-action scenario only recorded tab thrash / swipes \u2014 no product action";
|
|
83901
|
+
}
|
|
83902
|
+
const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
|
|
83903
|
+
if (asserts.length > 0 && asserts.every((a) => VOLATILE_BADGE_ASSERT_RE.test(actionText(a)) || NAV_CHROME_TAP_RE.test(actionText(a)) && !CONTENT_ACTION_VERB_RE.test(actionText(a)))) {
|
|
83904
|
+
return "content-action scenario only asserted tab chrome or a volatile badge count \u2014 assert the action outcome instead";
|
|
83905
|
+
}
|
|
83906
|
+
return null;
|
|
83907
|
+
}
|
|
83524
83908
|
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
83525
83909
|
return null;
|
|
83526
83910
|
}
|
|
83527
83911
|
if (isNavigationFocusedScenario(scenario)) {
|
|
83528
83912
|
return null;
|
|
83529
83913
|
}
|
|
83914
|
+
if (isCancelDiscardScenario(scenario)) {
|
|
83915
|
+
const hasCancelTap = actions.some((a) => {
|
|
83916
|
+
if (a.type !== "TAP")
|
|
83917
|
+
return false;
|
|
83918
|
+
return /\b(cancel|discard|close|dismiss|revert|undo|not\s+now)\b/.test(actionText(a));
|
|
83919
|
+
});
|
|
83920
|
+
const lastNavIdx = (() => {
|
|
83921
|
+
for (let i = actions.length - 1; i >= 0; i--) {
|
|
83922
|
+
const t = actions[i].type;
|
|
83923
|
+
if (t === "TAP" || t === "BACK" || t === "TYPE" || t === "CLEAR")
|
|
83924
|
+
return i;
|
|
83925
|
+
}
|
|
83926
|
+
return -1;
|
|
83927
|
+
})();
|
|
83928
|
+
const backAsDismiss = lastNavIdx >= 0 && actions[lastNavIdx].type === "BACK" && /\b(back|leave|exit|dismiss)\b/i.test(text) && /\b(without\s+saving|do\s+not\s+save|don't\s+save|discard|cancel)\b/i.test(text);
|
|
83929
|
+
if (!hasCancelTap && !backAsDismiss) {
|
|
83930
|
+
return "cancel/discard scenario did not record a cancel/dismiss action";
|
|
83931
|
+
}
|
|
83932
|
+
return null;
|
|
83933
|
+
}
|
|
83530
83934
|
if (isFormValidationScenario(scenario)) {
|
|
83531
83935
|
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
83532
83936
|
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
@@ -83620,30 +84024,42 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83620
84024
|
}
|
|
83621
84025
|
}
|
|
83622
84026
|
const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
|
|
83623
|
-
if (
|
|
83624
|
-
|
|
83625
|
-
|
|
83626
|
-
|
|
83627
|
-
|
|
83628
|
-
|
|
83629
|
-
|
|
83630
|
-
|
|
83631
|
-
|
|
83632
|
-
|
|
83633
|
-
|
|
83634
|
-
|
|
83635
|
-
|
|
83636
|
-
|
|
83637
|
-
|
|
83638
|
-
|
|
83639
|
-
|
|
83640
|
-
|
|
83641
|
-
|
|
83642
|
-
|
|
83643
|
-
|
|
83644
|
-
|
|
83645
|
-
|
|
83646
|
-
|
|
84027
|
+
if (isHappyPath) {
|
|
84028
|
+
let lastTypeIndex = -1;
|
|
84029
|
+
let lastAssertIndex = -1;
|
|
84030
|
+
actions.forEach((action, index) => {
|
|
84031
|
+
if (action.type === "TYPE")
|
|
84032
|
+
lastTypeIndex = index;
|
|
84033
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
84034
|
+
lastAssertIndex = index;
|
|
84035
|
+
});
|
|
84036
|
+
if (lastTypeIndex >= 0 && lastAssertIndex > lastTypeIndex) {
|
|
84037
|
+
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
84038
|
+
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
84039
|
+
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
84040
|
+
const lastTypeAction = actions[lastTypeIndex];
|
|
84041
|
+
const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
|
|
84042
|
+
const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
|
|
84043
|
+
if (index <= lastTypeIndex || index >= lastAssertIndex)
|
|
84044
|
+
return false;
|
|
84045
|
+
return isFormCommitAction(action);
|
|
84046
|
+
});
|
|
84047
|
+
if (screensDiffer && !commitAfterType) {
|
|
84048
|
+
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
84049
|
+
}
|
|
84050
|
+
}
|
|
84051
|
+
}
|
|
84052
|
+
if (isNavChromeThrashTrace(actions) && !isNavigationFocusedScenario(scenario) && !isFormMutationHappyPath(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario && !isHarnessOrVisibilityScenario(scenario)) {
|
|
84053
|
+
return "recording is only bottom-nav thrash and swipes \u2014 does not execute the scenario goal";
|
|
84054
|
+
}
|
|
84055
|
+
if (requiresFeatureProgress(scenario) && !isFormValidationScenario(scenario) && !isCredentialSignIn && !isAuthLikeScenario) {
|
|
84056
|
+
if (!actions.some((a) => isProductiveScenarioAction(a, text))) {
|
|
84057
|
+
return "happy-path / user-journey scenario did not record a productive product action (only chrome, gestures, or empty work)";
|
|
84058
|
+
}
|
|
84059
|
+
const asserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
|
|
84060
|
+
if (asserts.length > 0 && asserts.every((a) => VOLATILE_BADGE_ASSERT_RE.test(actionText(a)) || isStructuralOnlyAssert(a))) {
|
|
84061
|
+
return "happy-path / user-journey scenario only asserted volatile badges or structural roots \u2014 assert a durable feature outcome";
|
|
84062
|
+
}
|
|
83647
84063
|
}
|
|
83648
84064
|
return null;
|
|
83649
84065
|
}
|
|
@@ -83898,6 +84314,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83898
84314
|
lines.push("});");
|
|
83899
84315
|
return lines.join("\n");
|
|
83900
84316
|
}
|
|
84317
|
+
function isEphemeralContentAssertText(text) {
|
|
84318
|
+
const t = text.trim();
|
|
84319
|
+
if (!t)
|
|
84320
|
+
return false;
|
|
84321
|
+
if (/\b\d+\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|mo|mos|month|months|y|yr|yrs|year|years)\s+ago\b/i.test(t)) {
|
|
84322
|
+
return true;
|
|
84323
|
+
}
|
|
84324
|
+
if (/\b(just\s+now|moments?\s+ago|yesterday)\b/i.test(t))
|
|
84325
|
+
return true;
|
|
84326
|
+
if (/\btoday\s+at\s+\d{1,2}:\d{2}\b/i.test(t))
|
|
84327
|
+
return true;
|
|
84328
|
+
const feedish = /\b(followed|liked|commented|replied|mentioned|shared|posted|unread|notification|message from|new follower)\b/i.test(t) || /\b\d+\s*(s|m|h|d|w)\s+ago\b/i.test(t);
|
|
84329
|
+
if (feedish && t.length >= 70 && /[.!?].+\s/.test(t) && t.split(/\s+/).length >= 10) {
|
|
84330
|
+
return true;
|
|
84331
|
+
}
|
|
84332
|
+
if (t.length >= 40 && /\b(error\s+\d{3,}|generation failed|rate limit|quota exceeded)\b/i.test(t)) {
|
|
84333
|
+
return true;
|
|
84334
|
+
}
|
|
84335
|
+
return false;
|
|
84336
|
+
}
|
|
84337
|
+
function assertIdentityText(action) {
|
|
84338
|
+
return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
84339
|
+
}
|
|
83901
84340
|
function evaluateMobileAssertionStrength(actions) {
|
|
83902
84341
|
const modelAsserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
|
|
83903
84342
|
if (modelAsserts.length === 0) {
|
|
@@ -83933,6 +84372,24 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83933
84372
|
}
|
|
83934
84373
|
};
|
|
83935
84374
|
}
|
|
84375
|
+
const ephemeral = modelAsserts.filter((a) => isEphemeralContentAssertText(assertIdentityText(a)));
|
|
84376
|
+
const durable = modelAsserts.filter((a) => !isEphemeralContentAssertText(assertIdentityText(a)));
|
|
84377
|
+
if (ephemeral.length > 0 && durable.length === 0) {
|
|
84378
|
+
return {
|
|
84379
|
+
ok: false,
|
|
84380
|
+
severity: "block",
|
|
84381
|
+
reason: "only ephemeral content assertions (relative times, long feed/notification copy, or backend error strings) \u2014 assert durable chrome or run-scoped data instead",
|
|
84382
|
+
signals: {
|
|
84383
|
+
apiTest: false,
|
|
84384
|
+
interacted: true,
|
|
84385
|
+
mutated: actions.some((a) => a.type === "TYPE"),
|
|
84386
|
+
navOnly: false,
|
|
84387
|
+
hasOutcomeProof: false,
|
|
84388
|
+
errorBanner: false,
|
|
84389
|
+
terminal: null
|
|
84390
|
+
}
|
|
84391
|
+
};
|
|
84392
|
+
}
|
|
83936
84393
|
const code = mobileActionsToPseudoPlaywright(actions);
|
|
83937
84394
|
const interacted = actions.some((a) => a.type === "TAP" || a.type === "TYPE" || a.type === "CLEAR");
|
|
83938
84395
|
return (0, shared_1.validateAssertionStrength)(code, { interactionPerformed: interacted });
|
|
@@ -84236,7 +84693,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84236
84693
|
targetAppId,
|
|
84237
84694
|
credentials: credentialSecrets,
|
|
84238
84695
|
redactionCredentials: credentialValuesForRedaction,
|
|
84239
|
-
log: log2
|
|
84696
|
+
log: log2,
|
|
84697
|
+
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
|
|
84240
84698
|
});
|
|
84241
84699
|
consecutiveInfraFailures = 0;
|
|
84242
84700
|
const test = assembleTest({
|
|
@@ -84807,6 +85265,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
84807
85265
|
For a fix, return the COMPLETE steps array (not a diff).`;
|
|
84808
85266
|
var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
|
|
84809
85267
|
var RECOVERABLE_MAX = 3;
|
|
85268
|
+
var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
|
|
84810
85269
|
var BONUS_ATTEMPTS_CEIL = 4;
|
|
84811
85270
|
var DEFAULT_BONUS_ATTEMPTS = 2;
|
|
84812
85271
|
var MAX_LOG_CHARS = 4096;
|
|
@@ -85412,6 +85871,7 @@ ${args.directive}`);
|
|
|
85412
85871
|
const triedLocators = [];
|
|
85413
85872
|
let sameErrorCount = 0;
|
|
85414
85873
|
let lastErrorSignature = null;
|
|
85874
|
+
let consecutiveInvalidVerdicts = 0;
|
|
85415
85875
|
const markRecoverableTurn = () => {
|
|
85416
85876
|
if (recoverableUsed < RECOVERABLE_MAX) {
|
|
85417
85877
|
attemptCap += 1;
|
|
@@ -85485,11 +85945,22 @@ ${args.directive}`);
|
|
|
85485
85945
|
aiResponseCount += 1;
|
|
85486
85946
|
const parsed = parseHealVerdict(content);
|
|
85487
85947
|
if (!parsed) {
|
|
85488
|
-
|
|
85948
|
+
consecutiveInvalidVerdicts += 1;
|
|
85949
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict (${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 granting a recovery turn (${consecutiveInvalidVerdicts}/${MAX_CONSECUTIVE_INVALID_VERDICTS} consecutive)`);
|
|
85950
|
+
if (consecutiveInvalidVerdicts >= MAX_CONSECUTIVE_INVALID_VERDICTS) {
|
|
85951
|
+
options.onProgress?.(`Phase 1 aborted: ${consecutiveInvalidVerdicts} consecutive invalid verdicts \u2014 freeing device`);
|
|
85952
|
+
return {
|
|
85953
|
+
passed: false,
|
|
85954
|
+
verdict: "config_issue",
|
|
85955
|
+
healReason: `heal model returned ${consecutiveInvalidVerdicts} consecutive non-verdict responses (last ${content.length} chars, finish=${finishReason ?? "unknown"}) \u2014 aborting to free the device`,
|
|
85956
|
+
attemptsUsed: attempt
|
|
85957
|
+
};
|
|
85958
|
+
}
|
|
85489
85959
|
markRecoverableTurn();
|
|
85490
|
-
pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object.";
|
|
85960
|
+
pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object \u2014 no prose, no markdown fences.";
|
|
85491
85961
|
continue;
|
|
85492
85962
|
}
|
|
85963
|
+
consecutiveInvalidVerdicts = 0;
|
|
85493
85964
|
if (TERMINAL_VERDICTS.has(parsed.verdict)) {
|
|
85494
85965
|
return { passed: false, verdict: parsed.verdict, healReason: parsed.reason, attemptsUsed: attempt };
|
|
85495
85966
|
}
|
|
@@ -337169,10 +337640,11 @@ Fatal error: ${msg}`);
|
|
|
337169
337640
|
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
337170
337641
|
if (shouldPreserveAppData) {
|
|
337171
337642
|
try {
|
|
337172
|
-
await driver.
|
|
337643
|
+
await driver.launchApp();
|
|
337644
|
+
log2("App activated with preserved data (no terminate/clear).");
|
|
337173
337645
|
} catch (err) {
|
|
337174
337646
|
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
|
|
337175
|
-
log2(`App
|
|
337647
|
+
log2(`App activate failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
|
|
337176
337648
|
}
|
|
337177
337649
|
return false;
|
|
337178
337650
|
}
|
|
@@ -352875,9 +353347,9 @@ var require_dist2 = __commonJS({
|
|
|
352875
353347
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
352876
353348
|
};
|
|
352877
353349
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
352878
|
-
exports2.
|
|
352879
|
-
exports2.
|
|
352880
|
-
exports2.BrokeredInboxReader = exports2.applyPlusTag = exports2.deriveProjectInboxTag = exports2.deriveInboxTag = exports2.loadVerificationInboxConfig = exports2.VerificationInboxReader = exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.truncateAtLine = exports2.neutralizeUntrustedBlock = exports2.renderUntrustedKnowledgeBlock = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = void 0;
|
|
353350
|
+
exports2.filterLoginCaptureCredentials = exports2.buildLoginCaptureCredentials = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.filterFeatureGroupsToPathScope = exports2.isAuthFlowRoute = exports2.deriveSurveyPathScope = exports2.canonicalizeRoute = exports2.codeUsesInboxHelper = exports2.codeUsesTestInboxIdentity = exports2.codeSelfProvisionsRegistration = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.prepareMobileScenarioAppState = exports2.runMobileGeneratePhase = exports2.isLowValueMobileAutoDeepFeature = exports2.isPackageLeafAppCatchAll = exports2.isOpaqueMobileScreenName = exports2.isScrubbedPlaceholderName = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
|
|
353351
|
+
exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.validateAuthoredMobileTest = exports2.buildMobileRepoAuthorSystemRules = exports2.buildMobileRepoAuthorKickoff = exports2.buildMobileRepoAuthorContractMd = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.MOBILE_IMPORT_SCHEMA_VERSION = exports2.validateImportedMobileTest = exports2.buildMobileImportSystemRules = exports2.buildMobileImportKickoff = exports2.buildMobileImportContractMd = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = void 0;
|
|
353352
|
+
exports2.BrokeredInboxReader = exports2.applyPlusTag = exports2.deriveProjectInboxTag = exports2.deriveInboxTag = exports2.loadVerificationInboxConfig = exports2.VerificationInboxReader = exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.truncateAtLine = exports2.neutralizeUntrustedBlock = exports2.renderUntrustedKnowledgeBlock = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = void 0;
|
|
352881
353353
|
__exportStar(require_ai_provider(), exports2);
|
|
352882
353354
|
__exportStar(require_mobile_types(), exports2);
|
|
352883
353355
|
__exportStar(require_mobile_source_parser(), exports2);
|
|
@@ -352902,6 +353374,18 @@ var require_dist2 = __commonJS({
|
|
|
352902
353374
|
Object.defineProperty(exports2, "runMobileAIPlanner", { enumerable: true, get: function() {
|
|
352903
353375
|
return mobile_discovery_prompts_js_1.runMobileAIPlanner;
|
|
352904
353376
|
} });
|
|
353377
|
+
Object.defineProperty(exports2, "isScrubbedPlaceholderName", { enumerable: true, get: function() {
|
|
353378
|
+
return mobile_discovery_prompts_js_1.isScrubbedPlaceholderName;
|
|
353379
|
+
} });
|
|
353380
|
+
Object.defineProperty(exports2, "isOpaqueMobileScreenName", { enumerable: true, get: function() {
|
|
353381
|
+
return mobile_discovery_prompts_js_1.isOpaqueMobileScreenName;
|
|
353382
|
+
} });
|
|
353383
|
+
Object.defineProperty(exports2, "isPackageLeafAppCatchAll", { enumerable: true, get: function() {
|
|
353384
|
+
return mobile_discovery_prompts_js_1.isPackageLeafAppCatchAll;
|
|
353385
|
+
} });
|
|
353386
|
+
Object.defineProperty(exports2, "isLowValueMobileAutoDeepFeature", { enumerable: true, get: function() {
|
|
353387
|
+
return mobile_discovery_prompts_js_1.isLowValueMobileAutoDeepFeature;
|
|
353388
|
+
} });
|
|
352905
353389
|
var mobile_generator_js_1 = require_mobile_generator();
|
|
352906
353390
|
Object.defineProperty(exports2, "runMobileGeneratePhase", { enumerable: true, get: function() {
|
|
352907
353391
|
return mobile_generator_js_1.runMobileGeneratePhase;
|
|
@@ -353337,7 +353821,7 @@ var require_package4 = __commonJS({
|
|
|
353337
353821
|
"package.json"(exports2, module2) {
|
|
353338
353822
|
module2.exports = {
|
|
353339
353823
|
name: "@validate.qa/runner",
|
|
353340
|
-
version: "1.0.
|
|
353824
|
+
version: "1.0.86",
|
|
353341
353825
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353342
353826
|
bin: {
|
|
353343
353827
|
"validate-runner": "dist/cli.js"
|
|
@@ -354203,9 +354687,9 @@ async function waitForColdBootSettle(sessionPath) {
|
|
|
354203
354687
|
timeoutMs: MOBILE_COLD_BOOT_SETTLE_MS,
|
|
354204
354688
|
minStableFrames: 2,
|
|
354205
354689
|
pollMs: 400,
|
|
354206
|
-
// WebView
|
|
354207
|
-
//
|
|
354208
|
-
minElements:
|
|
354690
|
+
// Native splash / WebView shell is ~5–8 nodes; RN product screens and rich
|
|
354691
|
+
// onboarding typically flatten to 15+ once JS has painted.
|
|
354692
|
+
minElements: 15
|
|
354209
354693
|
});
|
|
354210
354694
|
}
|
|
354211
354695
|
async function getWindowSize(sessionPath) {
|
|
@@ -354354,20 +354838,33 @@ async function sendKeysToFocusedInput(sessionPath, value) {
|
|
|
354354
354838
|
}
|
|
354355
354839
|
async function typeText(sessionPath, value, bounds) {
|
|
354356
354840
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
354841
|
+
if (!elementId && bounds === void 0) {
|
|
354842
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
354843
|
+
}
|
|
354357
354844
|
if (elementId) {
|
|
354358
354845
|
try {
|
|
354359
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/
|
|
354846
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
354360
354847
|
method: "POST",
|
|
354361
|
-
body: JSON.stringify({
|
|
354848
|
+
body: JSON.stringify({})
|
|
354362
354849
|
});
|
|
354363
|
-
return;
|
|
354364
354850
|
} catch (err) {
|
|
354365
354851
|
if (isAppiumDriverDeadError(err)) throw err;
|
|
354366
354852
|
}
|
|
354367
|
-
} else if (bounds === void 0) {
|
|
354368
|
-
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
354369
354853
|
}
|
|
354370
|
-
|
|
354854
|
+
try {
|
|
354855
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
354856
|
+
return;
|
|
354857
|
+
} catch (err) {
|
|
354858
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354859
|
+
}
|
|
354860
|
+
if (elementId) {
|
|
354861
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
354862
|
+
method: "POST",
|
|
354863
|
+
body: JSON.stringify({ text: value })
|
|
354864
|
+
});
|
|
354865
|
+
return;
|
|
354866
|
+
}
|
|
354867
|
+
throw new Error("TYPE failed: could not send keys to the focused input.");
|
|
354371
354868
|
}
|
|
354372
354869
|
async function submitKeyboard(sessionPath, bounds) {
|
|
354373
354870
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
@@ -355236,7 +355733,7 @@ async function pressHome(sessionPath, platform3) {
|
|
|
355236
355733
|
// src/services/device-quarantine.ts
|
|
355237
355734
|
var QUARANTINE_MAX_MS = (() => {
|
|
355238
355735
|
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
355239
|
-
return Number.isFinite(raw) && raw > 0 ? raw :
|
|
355736
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2 * 60 * 1e3;
|
|
355240
355737
|
})();
|
|
355241
355738
|
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
355242
355739
|
var nextQuarantineToken = 1;
|
|
@@ -356733,6 +357230,12 @@ async function createSession(target) {
|
|
|
356733
357230
|
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356734
357231
|
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356735
357232
|
...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
357233
|
+
// WDA cold-start is flaky under dual-sim load (xcodebuild 65/75, SIGTERM).
|
|
357234
|
+
// Retry startup instead of failing the whole discovery session once.
|
|
357235
|
+
"appium:wdaStartupRetries": 4,
|
|
357236
|
+
"appium:wdaStartupRetryInterval": 2e3,
|
|
357237
|
+
"appium:wdaLaunchTimeout": 12e4,
|
|
357238
|
+
"appium:wdaConnectionTimeout": 12e4,
|
|
356736
357239
|
...parallelCaps,
|
|
356737
357240
|
...realCaps
|
|
356738
357241
|
} : {
|
|
@@ -356744,6 +357247,12 @@ async function createSession(target) {
|
|
|
356744
357247
|
"appium:noReset": true,
|
|
356745
357248
|
// See the iOS branch: the 60s default kills AI-driven discovery sessions.
|
|
356746
357249
|
"appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
|
|
357250
|
+
// Default UIA2 server launch is 30s — under dual-emu / post-quarantine
|
|
357251
|
+
// stress instrumentation often needs longer or the session never binds
|
|
357252
|
+
// ("instrumentation process cannot be initialized within 30000ms").
|
|
357253
|
+
"appium:uiautomator2ServerLaunchTimeout": 12e4,
|
|
357254
|
+
"appium:uiautomator2ServerInstallTimeout": 12e4,
|
|
357255
|
+
"appium:adbExecTimeout": 12e4,
|
|
356747
357256
|
"appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
|
|
356748
357257
|
...parallelCaps,
|
|
356749
357258
|
...realCaps
|
|
@@ -359512,6 +360021,9 @@ async function startSession(validated, res) {
|
|
|
359512
360021
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
359513
360022
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
359514
360023
|
"appium:noReset": true,
|
|
360024
|
+
// Keep pace with executor createSession: Expo "Open in App?" sheets and
|
|
360025
|
+
// permission dialogs otherwise freeze WDA for recording/mirror sessions.
|
|
360026
|
+
...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
|
|
359515
360027
|
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
359516
360028
|
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
359517
360029
|
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
@@ -359519,6 +360031,12 @@ async function startSession(validated, res) {
|
|
|
359519
360031
|
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
359520
360032
|
// die with "Simulator.app does not exist" while executor sessions work.
|
|
359521
360033
|
...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
360034
|
+
// Match executor createSession: tolerate WDA cold-start flakes.
|
|
360035
|
+
"appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
|
|
360036
|
+
"appium:wdaStartupRetries": 4,
|
|
360037
|
+
"appium:wdaStartupRetryInterval": 2e3,
|
|
360038
|
+
"appium:wdaLaunchTimeout": 12e4,
|
|
360039
|
+
"appium:wdaConnectionTimeout": 12e4,
|
|
359522
360040
|
...realCaps
|
|
359523
360041
|
} : {
|
|
359524
360042
|
platformName: "Android",
|
|
@@ -359527,6 +360045,10 @@ async function startSession(validated, res) {
|
|
|
359527
360045
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
359528
360046
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
359529
360047
|
"appium:noReset": true,
|
|
360048
|
+
"appium:newCommandTimeout": Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT) > 0 ? Math.floor(Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT)) : 600,
|
|
360049
|
+
"appium:uiautomator2ServerLaunchTimeout": 12e4,
|
|
360050
|
+
"appium:uiautomator2ServerInstallTimeout": 12e4,
|
|
360051
|
+
"appium:adbExecTimeout": 12e4,
|
|
359530
360052
|
"appium:chromedriverExecutableDir": ensureWritableChromedriverExecutableDir(),
|
|
359531
360053
|
...realCaps
|
|
359532
360054
|
};
|
|
@@ -360618,11 +361140,33 @@ var MobileBridgeDriver = class {
|
|
|
360618
361140
|
*/
|
|
360619
361141
|
async snapshot() {
|
|
360620
361142
|
return this.withSessionRecovery("snapshot", async () => {
|
|
360621
|
-
|
|
360622
|
-
|
|
360623
|
-
|
|
360624
|
-
|
|
360625
|
-
|
|
361143
|
+
let xml = await getPageSource(this.sessionPath);
|
|
361144
|
+
let screenshot = await getScreenshot(this.sessionPath);
|
|
361145
|
+
let windowSize = await getWindowSize(this.sessionPath);
|
|
361146
|
+
if (!xml) {
|
|
361147
|
+
try {
|
|
361148
|
+
await waitForUiSettle(this.sessionPath, {
|
|
361149
|
+
timeoutMs: 8e3,
|
|
361150
|
+
minStableFrames: 2,
|
|
361151
|
+
pollMs: 300,
|
|
361152
|
+
minElements: 1
|
|
361153
|
+
});
|
|
361154
|
+
} catch (err) {
|
|
361155
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
361156
|
+
}
|
|
361157
|
+
xml = await getPageSource(this.sessionPath);
|
|
361158
|
+
}
|
|
361159
|
+
if (!xml) {
|
|
361160
|
+
try {
|
|
361161
|
+
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
361162
|
+
await waitForColdBootSettle(this.sessionPath);
|
|
361163
|
+
} catch (err) {
|
|
361164
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
361165
|
+
}
|
|
361166
|
+
xml = await getPageSource(this.sessionPath);
|
|
361167
|
+
if (!screenshot) screenshot = await getScreenshot(this.sessionPath);
|
|
361168
|
+
if (!windowSize) windowSize = await getWindowSize(this.sessionPath);
|
|
361169
|
+
}
|
|
360626
361170
|
if (!xml && !screenshot && !windowSize) {
|
|
360627
361171
|
await this.assertSessionReachable();
|
|
360628
361172
|
}
|
|
@@ -363562,14 +364106,17 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363562
364106
|
// Android default) — a personal physical device must never have
|
|
363563
364107
|
// its app data wiped.
|
|
363564
364108
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
363565
|
-
|
|
364109
|
+
// Dual-device discovery + long AI turns can lose WDA/UIA2 more than
|
|
364110
|
+
// once per run. One recovery was too tight — the next snapshot
|
|
364111
|
+
// failed as "no UI tree" after a single WDA death.
|
|
364112
|
+
maxSessionRecoveries: 3,
|
|
363566
364113
|
recoverSession: async (cause) => {
|
|
363567
364114
|
if (discoveryTeardownStarted) {
|
|
363568
364115
|
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
363569
364116
|
}
|
|
363570
364117
|
const lost = session;
|
|
363571
364118
|
mobileOnLog(
|
|
363572
|
-
`[appium] discovery session lost (${cause.message}); recreating
|
|
364119
|
+
`[appium] discovery session lost (${cause.message}); recreating on the leased device`
|
|
363573
364120
|
);
|
|
363574
364121
|
if (lost) {
|
|
363575
364122
|
detachMirrorSession(lost.deviceId);
|