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