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