@validate.qa/runner 1.0.55 → 1.0.56
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 +317 -42
- package/dist/cli.mjs +317 -42
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2253,7 +2253,8 @@ function selectorCandidatesForStep(step) {
|
|
|
2253
2253
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2254
2254
|
const pushValueCandidates = (value) => {
|
|
2255
2255
|
candidates.push(selectorForValue(value));
|
|
2256
|
-
|
|
2256
|
+
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2257
|
+
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2257
2258
|
candidates.push(`id=${value}`);
|
|
2258
2259
|
}
|
|
2259
2260
|
};
|
|
@@ -7783,12 +7784,16 @@ var require_mobile_keyboard = __commonJS({
|
|
|
7783
7784
|
"use strict";
|
|
7784
7785
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
7785
7786
|
exports2.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
|
|
7787
|
+
exports2.hasFocusedEditableElement = hasFocusedEditableElement;
|
|
7786
7788
|
exports2.dismissKeyboardForRetry = dismissKeyboardForRetry;
|
|
7787
7789
|
var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
|
|
7788
7790
|
var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
|
|
7789
7791
|
function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
|
|
7790
7792
|
if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
|
|
7791
7793
|
return false;
|
|
7794
|
+
return hasFocusedEditableElement(elements);
|
|
7795
|
+
}
|
|
7796
|
+
function hasFocusedEditableElement(elements) {
|
|
7792
7797
|
return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
|
|
7793
7798
|
}
|
|
7794
7799
|
async function dismissKeyboardForRetry(driver) {
|
|
@@ -75293,6 +75298,9 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75293
75298
|
let totalBreadthContinuationGranted = 0;
|
|
75294
75299
|
let progressMarkAtLastBreadthContinuation = null;
|
|
75295
75300
|
let lastPostSubmitConfirmationToken = null;
|
|
75301
|
+
let keyboardRecoveryScreenId = null;
|
|
75302
|
+
let keyboardRecoverySameScreenTurns = 0;
|
|
75303
|
+
let lastKeyboardRecoveryIteration = -Infinity;
|
|
75296
75304
|
const maybeGrantBreadthContinuation = (iteration, breadthCoverage) => {
|
|
75297
75305
|
if (mode !== "survey" || breadthCoverage.complete || iteration < Math.floor(maxIterations * 0.8))
|
|
75298
75306
|
return 0;
|
|
@@ -75363,6 +75371,29 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75363
75371
|
log2(` Post-submit confirmation snapshot failed non-fatally: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
75364
75372
|
}
|
|
75365
75373
|
}
|
|
75374
|
+
{
|
|
75375
|
+
const rawScreenId = state2.getCurrentScreenId();
|
|
75376
|
+
const webIdx = rawScreenId ? rawScreenId.indexOf(".web-") : -1;
|
|
75377
|
+
const hashIdx = rawScreenId ? rawScreenId.indexOf("#") : -1;
|
|
75378
|
+
const currentScreenId = rawScreenId && webIdx >= 0 ? `${rawScreenId.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${rawScreenId.slice(webIdx)}` : rawScreenId;
|
|
75379
|
+
if (currentScreenId && currentScreenId === keyboardRecoveryScreenId) {
|
|
75380
|
+
keyboardRecoverySameScreenTurns += 1;
|
|
75381
|
+
} else {
|
|
75382
|
+
keyboardRecoveryScreenId = currentScreenId ?? null;
|
|
75383
|
+
keyboardRecoverySameScreenTurns = 0;
|
|
75384
|
+
}
|
|
75385
|
+
const keyboardLikelyOpen = (0, mobile_keyboard_js_1.hasFocusedEditableElement)(getLatest()?.screen.elements ?? []);
|
|
75386
|
+
if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
|
|
75387
|
+
lastKeyboardRecoveryIteration = iteration;
|
|
75388
|
+
try {
|
|
75389
|
+
await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
75390
|
+
log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen with a focused input).");
|
|
75391
|
+
transientDirectives.push("HARNESS NOTE: the on-screen keyboard was just auto-dismissed because multiple turns stalled on this screen \u2014 controls it occluded (e.g. the form submit button) may now be visible. Call mobile_snapshot, then TAP the commit/submit control instead of re-filling fields.");
|
|
75392
|
+
} catch (error2) {
|
|
75393
|
+
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
|
|
75394
|
+
}
|
|
75395
|
+
}
|
|
75396
|
+
}
|
|
75366
75397
|
if (iteration > 6 && state2.isStaleProgress()) {
|
|
75367
75398
|
transientDirectives.push("You have made no screen-graph progress recently. If the accessible content is covered, call finish_exploration. Otherwise swipe to reveal off-screen content or tap a not-yet-explored element.");
|
|
75368
75399
|
}
|
|
@@ -79945,10 +79976,34 @@ ${constraints.join("\n")}`);
|
|
|
79945
79976
|
const via = triggerLabel ? ` via ${JSON.stringify(triggerLabel)}` : "";
|
|
79946
79977
|
group.flows?.push(`${nameById.get(transition.fromScreenId) ?? transition.fromScreenId} \u2192 ${nameById.get(transition.toScreenId) ?? transition.toScreenId} (${transition.action}${via})`);
|
|
79947
79978
|
}
|
|
79948
|
-
return [...groups.values()].map(({ realm
|
|
79949
|
-
|
|
79950
|
-
|
|
79951
|
-
|
|
79979
|
+
return [...groups.values()].map(({ realm, ...group }) => {
|
|
79980
|
+
const seenPageKeys = /* @__PURE__ */ new Set();
|
|
79981
|
+
const routes = [];
|
|
79982
|
+
const pages = [];
|
|
79983
|
+
group.routes.forEach((route2, index) => {
|
|
79984
|
+
const webIdx = route2.indexOf(".web-");
|
|
79985
|
+
const hashIdx = route2.indexOf("#");
|
|
79986
|
+
const key = webIdx >= 0 ? `${route2.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${route2.slice(webIdx)}` : route2;
|
|
79987
|
+
if (seenPageKeys.has(key))
|
|
79988
|
+
return;
|
|
79989
|
+
seenPageKeys.add(key);
|
|
79990
|
+
routes.push(route2);
|
|
79991
|
+
const page = group.pages?.[index];
|
|
79992
|
+
if (page)
|
|
79993
|
+
pages.push(page);
|
|
79994
|
+
});
|
|
79995
|
+
const flows = [...new Set(group.flows ?? [])];
|
|
79996
|
+
const screenNames = [...new Set(routes.map((route2) => nameById.get(route2) ?? route2))];
|
|
79997
|
+
const preview = screenNames.slice(0, 3).join(", ");
|
|
79998
|
+
const description = `${realm === "authenticated" ? "Authenticated" : "Public"} mobile area "${group.name}" \u2014 ${routes.length} screen(s)${preview ? ` (${preview}${screenNames.length > 3 ? ", \u2026" : ""})` : ""}${flows.length > 0 ? `, ${flows.length} observed transition(s)` : ""}.`;
|
|
79999
|
+
return {
|
|
80000
|
+
...group,
|
|
80001
|
+
routes,
|
|
80002
|
+
pages,
|
|
80003
|
+
description,
|
|
80004
|
+
flows
|
|
80005
|
+
};
|
|
80006
|
+
});
|
|
79952
80007
|
}
|
|
79953
80008
|
function normalizeMobileSurveyFeatureGroups(groups, graph, appId, existingFeatureNames = []) {
|
|
79954
80009
|
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
|
|
@@ -82130,6 +82185,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82130
82185
|
function weakAssertionTargetReason(element) {
|
|
82131
82186
|
if (isAlwaysPresentContainer(element))
|
|
82132
82187
|
return "always-present app/system container";
|
|
82188
|
+
const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
|
|
82189
|
+
if (!hasIdentitySignal)
|
|
82190
|
+
return "no identity locator (accessibility id / resource id / text) for the assert gate";
|
|
82133
82191
|
const structuralIdentity = [
|
|
82134
82192
|
element.accessibilityId,
|
|
82135
82193
|
element.resourceId,
|
|
@@ -83632,6 +83690,94 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
83632
83690
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
83633
83691
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
83634
83692
|
}
|
|
83693
|
+
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
83694
|
+
function isIdentityEqualityXpath(value) {
|
|
83695
|
+
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
83696
|
+
}
|
|
83697
|
+
function stepLacksIdentityAnchor(step) {
|
|
83698
|
+
const metadata = step.metadata;
|
|
83699
|
+
if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
|
|
83700
|
+
return false;
|
|
83701
|
+
const target = step.target;
|
|
83702
|
+
if (!target)
|
|
83703
|
+
return true;
|
|
83704
|
+
const bundle = target.locatorBundle;
|
|
83705
|
+
if (bundle?.accessibilityId || bundle?.resourceId)
|
|
83706
|
+
return false;
|
|
83707
|
+
if (bundle?.text?.trim() || bundle?.contentDesc?.trim())
|
|
83708
|
+
return false;
|
|
83709
|
+
if (bundle?.xpath && isIdentityEqualityXpath(bundle.xpath))
|
|
83710
|
+
return false;
|
|
83711
|
+
for (const raw of [target.primary, ...target.fallbacks ?? []]) {
|
|
83712
|
+
if (typeof raw !== "string")
|
|
83713
|
+
continue;
|
|
83714
|
+
const value = raw.trim();
|
|
83715
|
+
if (!value)
|
|
83716
|
+
continue;
|
|
83717
|
+
if (value.startsWith("css="))
|
|
83718
|
+
continue;
|
|
83719
|
+
if (value.startsWith("~") && value.length > 1)
|
|
83720
|
+
return false;
|
|
83721
|
+
if (value.startsWith("id=") && value.length > 3)
|
|
83722
|
+
return false;
|
|
83723
|
+
if (value.startsWith("//") || value.startsWith("(")) {
|
|
83724
|
+
if (isIdentityEqualityXpath(value))
|
|
83725
|
+
return false;
|
|
83726
|
+
continue;
|
|
83727
|
+
}
|
|
83728
|
+
if (value.startsWith("/"))
|
|
83729
|
+
continue;
|
|
83730
|
+
return false;
|
|
83731
|
+
}
|
|
83732
|
+
return true;
|
|
83733
|
+
}
|
|
83734
|
+
async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
|
|
83735
|
+
if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
|
|
83736
|
+
return null;
|
|
83737
|
+
const identityless = steps.filter(stepLacksIdentityAnchor);
|
|
83738
|
+
if (identityless.length === 0)
|
|
83739
|
+
return null;
|
|
83740
|
+
const isAssert = (step) => /^assert/i.test(step.action);
|
|
83741
|
+
if (!identityless.every(isAssert)) {
|
|
83742
|
+
return {
|
|
83743
|
+
passed: false,
|
|
83744
|
+
verdict: "unhealable_dynamic",
|
|
83745
|
+
healReason: "identity gate: an action step carries no identity locator (accessibility id / resource id / text); the recording lacks the anchors replay requires \u2014 re-record the step"
|
|
83746
|
+
};
|
|
83747
|
+
}
|
|
83748
|
+
const pruned = steps.filter((step) => !stepLacksIdentityAnchor(step));
|
|
83749
|
+
if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
|
|
83750
|
+
return {
|
|
83751
|
+
passed: false,
|
|
83752
|
+
verdict: "unhealable_dynamic",
|
|
83753
|
+
healReason: "identity gate: the only assertion(s) carry no identity locator, so the test cannot prove its outcome as recorded \u2014 re-record with an identifiable anchor"
|
|
83754
|
+
};
|
|
83755
|
+
}
|
|
83756
|
+
const reindexed = pruned.map((step, order) => ({ ...step, order }));
|
|
83757
|
+
try {
|
|
83758
|
+
const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
|
|
83759
|
+
if (!verify.passed)
|
|
83760
|
+
return null;
|
|
83761
|
+
let healedAppiumCode;
|
|
83762
|
+
if (ctx.testName) {
|
|
83763
|
+
try {
|
|
83764
|
+
healedAppiumCode = (0, shared_1.buildAppiumCode)(ctx.testName, toDeterministicTarget(target), reindexed);
|
|
83765
|
+
} catch {
|
|
83766
|
+
}
|
|
83767
|
+
}
|
|
83768
|
+
return {
|
|
83769
|
+
passed: true,
|
|
83770
|
+
verdict: "fix",
|
|
83771
|
+
healedSteps: reindexed,
|
|
83772
|
+
...healedAppiumCode ? { healedAppiumCode } : {},
|
|
83773
|
+
healReason: `identity gate: dropped ${identityless.length} assert step(s) recorded without any identity locator; pruned test verified green in one replay`,
|
|
83774
|
+
stepResults: verify.stepResults,
|
|
83775
|
+
logs: verify.logs
|
|
83776
|
+
};
|
|
83777
|
+
} catch {
|
|
83778
|
+
return null;
|
|
83779
|
+
}
|
|
83780
|
+
}
|
|
83635
83781
|
function stripCodeFences(text) {
|
|
83636
83782
|
const trimmed = text.trim();
|
|
83637
83783
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -83972,7 +84118,17 @@ ${args.directive}`);
|
|
|
83972
84118
|
break;
|
|
83973
84119
|
}
|
|
83974
84120
|
}
|
|
84121
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84122
|
+
return {
|
|
84123
|
+
passed: false,
|
|
84124
|
+
verdict: "config_issue",
|
|
84125
|
+
healReason: `mobile heal deadline exceeded during Phase 1 (after ${attempt} attempt(s)) \u2014 aborting before further model turns`,
|
|
84126
|
+
attemptsUsed: attempt,
|
|
84127
|
+
lastError
|
|
84128
|
+
};
|
|
84129
|
+
}
|
|
83975
84130
|
attempt += 1;
|
|
84131
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}/${attemptCap} \u2014 asking the model for a script fix...`);
|
|
83976
84132
|
const userContent = buildUserContent({
|
|
83977
84133
|
attempt,
|
|
83978
84134
|
baseSteps,
|
|
@@ -83999,6 +84155,7 @@ ${args.directive}`);
|
|
|
83999
84155
|
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
84000
84156
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84001
84157
|
} catch {
|
|
84158
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}: model call failed in transport \u2014 granting a recovery turn`);
|
|
84002
84159
|
markRecoverableTurn();
|
|
84003
84160
|
pendingDirective = "The previous attempt did not reach the model. Re-analyze and return one valid JSON verdict.";
|
|
84004
84161
|
continue;
|
|
@@ -84173,6 +84330,9 @@ ${args.directive}`);
|
|
|
84173
84330
|
const preflight = await tryRegistryPreflight(steps, target, ctx, options);
|
|
84174
84331
|
if (preflight)
|
|
84175
84332
|
return preflight;
|
|
84333
|
+
const identityRepair = await tryIdentityGateStaticRepair(steps, target, ctx, options);
|
|
84334
|
+
if (identityRepair)
|
|
84335
|
+
return identityRepair;
|
|
84176
84336
|
const script = await executeMobileScriptDrivenHeal(steps, target, ctx, options);
|
|
84177
84337
|
if (script.providerUnavailable) {
|
|
84178
84338
|
const fallback = await tryDeterministicProviderFallback({ steps, target, ctx, options });
|
|
@@ -84346,6 +84506,14 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
84346
84506
|
let bestStepResults;
|
|
84347
84507
|
let aiResponseCount = 0;
|
|
84348
84508
|
for (let pass = 1; pass <= maxPasses; pass += 1) {
|
|
84509
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84510
|
+
return {
|
|
84511
|
+
passed: false,
|
|
84512
|
+
verdict: "config_issue",
|
|
84513
|
+
healReason: `mobile heal deadline exceeded during live (Phase 2) healing at pass ${pass}/${maxPasses}`
|
|
84514
|
+
};
|
|
84515
|
+
}
|
|
84516
|
+
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
84349
84517
|
let snap;
|
|
84350
84518
|
try {
|
|
84351
84519
|
snap = await liveDriver.snapshot();
|
|
@@ -319090,7 +319258,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319090
319258
|
"../runner-core/dist/services/survey-link-frontier.js"(exports2) {
|
|
319091
319259
|
"use strict";
|
|
319092
319260
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
319093
|
-
exports2.FINISH_GATE_MIN_BUDGET_PCT = exports2.MAX_FINISH_REDIRECTS = void 0;
|
|
319261
|
+
exports2.FINISH_ESCAPE_BOUNCE_STREAK = exports2.FINISH_GATE_MIN_BUDGET_PCT = exports2.MAX_FINISH_REDIRECTS = void 0;
|
|
319262
|
+
exports2.initialFinishBounceState = initialFinishBounceState;
|
|
319263
|
+
exports2.evaluateFinishBounce = evaluateFinishBounce;
|
|
319264
|
+
exports2.recordFinishBounce = recordFinishBounce;
|
|
319094
319265
|
exports2.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
|
|
319095
319266
|
exports2.extractSnapshotLinks = extractSnapshotLinks;
|
|
319096
319267
|
exports2.deriveLinkScope = deriveLinkScope;
|
|
@@ -319107,6 +319278,17 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319107
319278
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
319108
319279
|
exports2.MAX_FINISH_REDIRECTS = 2;
|
|
319109
319280
|
exports2.FINISH_GATE_MIN_BUDGET_PCT = 0.2;
|
|
319281
|
+
exports2.FINISH_ESCAPE_BOUNCE_STREAK = 3;
|
|
319282
|
+
function initialFinishBounceState() {
|
|
319283
|
+
return { streak: 0, pagesBaseline: 0 };
|
|
319284
|
+
}
|
|
319285
|
+
function evaluateFinishBounce(prev, pagesVisited, threshold = exports2.FINISH_ESCAPE_BOUNCE_STREAK) {
|
|
319286
|
+
const base2 = pagesVisited > prev.pagesBaseline ? { streak: 0, pagesBaseline: pagesVisited } : prev;
|
|
319287
|
+
return { state: base2, escape: base2.streak >= threshold };
|
|
319288
|
+
}
|
|
319289
|
+
function recordFinishBounce(prev) {
|
|
319290
|
+
return { streak: prev.streak + 1, pagesBaseline: prev.pagesBaseline };
|
|
319291
|
+
}
|
|
319110
319292
|
function isSurveyLinkFrontierEnabled() {
|
|
319111
319293
|
const v = process.env.DISCOVERY_SURVEY_LINK_FRONTIER;
|
|
319112
319294
|
return v === "true" || v === "1";
|
|
@@ -323295,6 +323477,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
323295
323477
|
const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
|
|
323296
323478
|
const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
|
|
323297
323479
|
let finishRedirects = 0;
|
|
323480
|
+
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
323298
323481
|
let lastLandedPageUrl = null;
|
|
323299
323482
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
323300
323483
|
function ingestLinksFromText(rawText) {
|
|
@@ -323532,12 +323715,14 @@ ${inboxPromptBlock}`;
|
|
|
323532
323715
|
const item = state2.frontier.get(r);
|
|
323533
323716
|
return item?.source === "link-seen" && item.attempted === true;
|
|
323534
323717
|
};
|
|
323535
|
-
const
|
|
323718
|
+
const isSatisfied = (r) => (0, survey_link_frontier_js_1.visitedSatisfies)(state2, r);
|
|
323719
|
+
const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r) && !isSatisfied(r));
|
|
323536
323720
|
const rankedSet = new Set(ranked);
|
|
323537
323721
|
const navigatedUncaptured = [];
|
|
323538
323722
|
for (const r of state2.frontier.keys()) {
|
|
323539
|
-
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
|
|
323723
|
+
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r) && !isSatisfied(r)) {
|
|
323540
323724
|
navigatedUncaptured.push(r);
|
|
323725
|
+
}
|
|
323541
323726
|
}
|
|
323542
323727
|
return ranked.concat(navigatedUncaptured);
|
|
323543
323728
|
};
|
|
@@ -324090,6 +324275,7 @@ ${currentSummary}`;
|
|
|
324090
324275
|
}
|
|
324091
324276
|
const toolResults = [];
|
|
324092
324277
|
let finishBouncedThisBatch = false;
|
|
324278
|
+
let finishStreakCountedThisBatch = false;
|
|
324093
324279
|
const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
324094
324280
|
const { secrets: secretsOnlyForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
|
|
324095
324281
|
const allowedCredentialValues = inboxState ? [inboxState.email] : [];
|
|
@@ -324268,25 +324454,41 @@ ${currentSummary}`;
|
|
|
324268
324454
|
}
|
|
324269
324455
|
logs2.push(` Captured page: ${normalized.route}, ${normalized.provenCommands.length} proven commands${normalized.alreadyCaptured ? " (RE-VISIT)" : ""}`);
|
|
324270
324456
|
const transitionHint = !normalized.alreadyCaptured && isSurveyMode ? { tip: `Also call record_transition with fromRoute, toRoute, triggerType, and triggerLabel (the exact link text you clicked to get here).` } : {};
|
|
324271
|
-
const
|
|
324272
|
-
|
|
324273
|
-
|
|
324274
|
-
{
|
|
324275
|
-
|
|
324276
|
-
|
|
324277
|
-
|
|
324278
|
-
|
|
324279
|
-
|
|
324280
|
-
|
|
324281
|
-
|
|
324282
|
-
|
|
324283
|
-
|
|
324284
|
-
|
|
324285
|
-
|
|
324286
|
-
|
|
324287
|
-
|
|
324288
|
-
|
|
324289
|
-
|
|
324457
|
+
const progressLine = `${state2.pagesVisited} pages, ${state2.transitionsFound} transitions, ${state2.recordedJourneys.length} tests | iteration ${iteration}/${maxIterations} (${maxIterations - iteration} remaining)`;
|
|
324458
|
+
if (isSurveyMode && normalized.alreadyCaptured && normalized.observations.length === 0 && (adaptiveActive || linkFrontierEnabled)) {
|
|
324459
|
+
const nextRoutes = (adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route)).slice(0, 6);
|
|
324460
|
+
const steer = nextRoutes.length > 0 ? `Already mapped \u2014 no new intel on this page. Still un-captured: ${nextRoutes.join(", ")}. Navigate to one and capture_page, or call finish_exploration if none remain.` : "Already mapped and no un-captured routes remain. Call finish_exploration.";
|
|
324461
|
+
toolResults.push({
|
|
324462
|
+
role: "tool",
|
|
324463
|
+
tool_call_id: toolCall.id,
|
|
324464
|
+
content: JSON.stringify({
|
|
324465
|
+
acknowledged: true,
|
|
324466
|
+
alreadyCaptured: true,
|
|
324467
|
+
note: steer,
|
|
324468
|
+
progress: progressLine
|
|
324469
|
+
})
|
|
324470
|
+
});
|
|
324471
|
+
} else {
|
|
324472
|
+
const lastAction = normalized.observations.length > 0 ? normalized.observations[normalized.observations.length - 1].action : void 0;
|
|
324473
|
+
const snapshotBlock = (0, snapshot_observation_js_1.renderCompactSnapshotBlock)(
|
|
324474
|
+
normalized,
|
|
324475
|
+
{ lastAction },
|
|
324476
|
+
// G10: no explicit elementLimit — let the flag-aware deep default govern
|
|
324477
|
+
// (flags-off 6 = byte-identical; richRender 8, so demoted form controls fit).
|
|
324478
|
+
{ formLimit: 3, observationLimit: 3, mode: options?.mode ?? "full", recoveredClickables }
|
|
324479
|
+
);
|
|
324480
|
+
toolResults.push({
|
|
324481
|
+
role: "tool",
|
|
324482
|
+
tool_call_id: toolCall.id,
|
|
324483
|
+
content: JSON.stringify({
|
|
324484
|
+
acknowledged: true,
|
|
324485
|
+
...normalized.alreadyCaptured ? { note: "Already captured - move on to unvisited pages." } : {},
|
|
324486
|
+
...transitionHint,
|
|
324487
|
+
snapshot: snapshotBlock,
|
|
324488
|
+
progress: progressLine
|
|
324489
|
+
})
|
|
324490
|
+
});
|
|
324491
|
+
}
|
|
324290
324492
|
} catch (err) {
|
|
324291
324493
|
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
324292
324494
|
logs2.push(` Error persisting page: ${errMsg2}`);
|
|
@@ -324703,7 +324905,18 @@ ${currentSummary}`;
|
|
|
324703
324905
|
continue;
|
|
324704
324906
|
}
|
|
324705
324907
|
if (toolName === "finish_exploration") {
|
|
324706
|
-
|
|
324908
|
+
const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
|
|
324909
|
+
const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
|
|
324910
|
+
finishBounce = fb.state;
|
|
324911
|
+
const forceAcceptFinish = fb.escape;
|
|
324912
|
+
if (forceAcceptFinish) {
|
|
324913
|
+
const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
|
|
324914
|
+
const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
324915
|
+
logs2.push(` [finish-escape] accepting finish after ${finishBounce.streak} consecutive no-progress bounce(s)` + (stuck.length > 0 ? `; ${stuck.length} route(s) unresolved: ${stuck.slice(0, 6).join(", ")}${stuck.length > 6 ? " \u2026" : ""}` : ""));
|
|
324916
|
+
if (reasonText)
|
|
324917
|
+
logs2.push(` [finish-escape] agent's stated reason: ${reasonText}`);
|
|
324918
|
+
}
|
|
324919
|
+
if (!forceAcceptFinish && linkFrontierEnabled) {
|
|
324707
324920
|
const blocking = (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2);
|
|
324708
324921
|
const budgetRemainingPct = maxIterations > 0 ? (maxIterations - iteration) / maxIterations : 0;
|
|
324709
324922
|
if (blocking.length > 0 && budgetRemainingPct >= survey_link_frontier_js_1.FINISH_GATE_MIN_BUDGET_PCT && (finishRedirects < survey_link_frontier_js_1.MAX_FINISH_REDIRECTS || finishBouncedThisBatch)) {
|
|
@@ -324722,13 +324935,17 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324722
324935
|
reason: `Not done yet - ${blocking.length} route(s) were visible as links but never visited or captured: ${targets}. A survey must map every reachable top-level page. Navigate to each, capture_page + record_transition, then call finish_exploration. If one is genuinely unreachable (requires credentials you don't have, or leaves the app), note that in your summary and move on.`
|
|
324723
324936
|
})
|
|
324724
324937
|
});
|
|
324938
|
+
if (!finishStreakCountedThisBatch) {
|
|
324939
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
324940
|
+
finishStreakCountedThisBatch = true;
|
|
324941
|
+
}
|
|
324725
324942
|
continue;
|
|
324726
324943
|
}
|
|
324727
324944
|
if (blocking.length > 0) {
|
|
324728
324945
|
logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
|
|
324729
324946
|
}
|
|
324730
324947
|
}
|
|
324731
|
-
if (adaptiveActive && !adaptiveComplete) {
|
|
324948
|
+
if (!forceAcceptFinish && adaptiveActive && !adaptiveComplete) {
|
|
324732
324949
|
refreshAdaptiveDiscovery();
|
|
324733
324950
|
const remainingList = adaptiveDebtRoutes();
|
|
324734
324951
|
if (remainingList.length > 0 && iteration < adaptiveHardCap) {
|
|
@@ -324736,10 +324953,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324736
324953
|
if (iteration >= adaptiveDecisionArmedAt) {
|
|
324737
324954
|
const { directive, keepGoing } = runBudgetController();
|
|
324738
324955
|
if (keepGoing) {
|
|
324739
|
-
reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
|
|
324956
|
+
reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
|
|
324740
324957
|
}
|
|
324741
324958
|
} else {
|
|
324742
|
-
reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
|
|
324959
|
+
reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
|
|
324743
324960
|
}
|
|
324744
324961
|
if (reason) {
|
|
324745
324962
|
toolResults.push({
|
|
@@ -324748,6 +324965,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324748
324965
|
content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
|
|
324749
324966
|
});
|
|
324750
324967
|
logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
|
|
324968
|
+
if (!finishStreakCountedThisBatch) {
|
|
324969
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
324970
|
+
finishStreakCountedThisBatch = true;
|
|
324971
|
+
}
|
|
324751
324972
|
continue;
|
|
324752
324973
|
}
|
|
324753
324974
|
}
|
|
@@ -349375,7 +349596,7 @@ var require_package4 = __commonJS({
|
|
|
349375
349596
|
"package.json"(exports2, module2) {
|
|
349376
349597
|
module2.exports = {
|
|
349377
349598
|
name: "@validate.qa/runner",
|
|
349378
|
-
version: "1.0.
|
|
349599
|
+
version: "1.0.56",
|
|
349379
349600
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
349380
349601
|
bin: {
|
|
349381
349602
|
"validate-runner": "dist/cli.js"
|
|
@@ -350989,6 +351210,17 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
350989
351210
|
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
350990
351211
|
throw new Error(`WEBVIEW element not found for css=${css}`);
|
|
350991
351212
|
}
|
|
351213
|
+
try {
|
|
351214
|
+
const scriptResult = await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
351215
|
+
method: "POST",
|
|
351216
|
+
body: JSON.stringify({
|
|
351217
|
+
script: "var el = arguments[0];var form = el && (el.form || (el.closest ? el.closest('form') : null));if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); return 'request-submit'; }return 'no-form';",
|
|
351218
|
+
args: [{ ELEMENT: elementId, "element-6066-11e4-a52e-4f735466cecf": elementId }]
|
|
351219
|
+
})
|
|
351220
|
+
});
|
|
351221
|
+
if (readValue(scriptResult) === "request-submit") return;
|
|
351222
|
+
} catch {
|
|
351223
|
+
}
|
|
350992
351224
|
await appiumRequest(`${sessionPath}/element/${elementId}/click`, {
|
|
350993
351225
|
method: "POST",
|
|
350994
351226
|
body: "{}"
|
|
@@ -350998,6 +351230,14 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
350998
351230
|
body: JSON.stringify({ text: WEBDRIVER_ENTER_KEY, value: [WEBDRIVER_ENTER_KEY] })
|
|
350999
351231
|
});
|
|
351000
351232
|
}
|
|
351233
|
+
async function hideKeyboardIfShown(sessionPath) {
|
|
351234
|
+
try {
|
|
351235
|
+
const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
|
|
351236
|
+
if (readValue(shown) !== true) return;
|
|
351237
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, { method: "POST", body: "{}" });
|
|
351238
|
+
} catch {
|
|
351239
|
+
}
|
|
351240
|
+
}
|
|
351001
351241
|
async function withWebViewContext(sessionPath, webviewName, body, options = {}) {
|
|
351002
351242
|
const previous = await getAppContext(sessionPath) ?? "NATIVE_APP";
|
|
351003
351243
|
let bodyCompleted = false;
|
|
@@ -355799,6 +356039,7 @@ var MobileBridgeDriver = class {
|
|
|
355799
356039
|
() => typeCssInCurrentContext(this.sessionPath, css, value),
|
|
355800
356040
|
this.webViewEntryOptions(webviewContext)
|
|
355801
356041
|
);
|
|
356042
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355802
356043
|
});
|
|
355803
356044
|
}
|
|
355804
356045
|
async webSubmitKeyboard(webviewContext, css) {
|
|
@@ -355809,6 +356050,7 @@ var MobileBridgeDriver = class {
|
|
|
355809
356050
|
() => submitCssKeyboardInCurrentContext(this.sessionPath, css),
|
|
355810
356051
|
this.webViewEntryOptions(webviewContext)
|
|
355811
356052
|
);
|
|
356053
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355812
356054
|
});
|
|
355813
356055
|
}
|
|
355814
356056
|
};
|
|
@@ -356248,7 +356490,7 @@ var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bea
|
|
|
356248
356490
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
356249
356491
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
356250
356492
|
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
356251
|
-
var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION;
|
|
356493
|
+
var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION ?? (RUNNER_VERSION !== "0.0.0" ? `npm-v${RUNNER_VERSION}` : void 0);
|
|
356252
356494
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
356253
356495
|
var DEFAULT_TYPE_LIMITS = {
|
|
356254
356496
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -356899,7 +357141,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
356899
357141
|
}, assignedDeviceId, mobileLeaseError) {
|
|
356900
357142
|
const runMode = run2.mode ?? "run";
|
|
356901
357143
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
356902
|
-
|
|
357144
|
+
const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
|
|
357145
|
+
const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
|
|
357146
|
+
log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
|
|
356903
357147
|
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
356904
357148
|
sessionId: `${run2.runId}`,
|
|
356905
357149
|
mode: toLiveBrowserMode(runMode),
|
|
@@ -358733,9 +358977,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358733
358977
|
}
|
|
358734
358978
|
const reproduceError = reproduce.error;
|
|
358735
358979
|
const modelOverride = run2.healerModel ?? void 0;
|
|
358980
|
+
const healDeadlineAt = Date.now() + Math.max(6e4, envInt("MOBILE_HEAL_DEADLINE_MS", 20 * 6e4));
|
|
358736
358981
|
const healerOptions = {
|
|
358737
358982
|
...modelOverride ? { model: modelOverride } : {},
|
|
358983
|
+
deadlineAt: healDeadlineAt,
|
|
358738
358984
|
runNativeTest,
|
|
358985
|
+
// Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
|
|
358986
|
+
// for many minutes of model calls with zero device traffic, which
|
|
358987
|
+
// previously printed nothing between the start banner and the verdict.
|
|
358988
|
+
onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
|
|
358739
358989
|
selectorRegistryContext: run2.selectorRegistryContext ?? null,
|
|
358740
358990
|
runLiveHeal: async (a) => {
|
|
358741
358991
|
let liveSession = null;
|
|
@@ -358778,6 +359028,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358778
359028
|
}
|
|
358779
359029
|
return bridgeDriver;
|
|
358780
359030
|
};
|
|
359031
|
+
if (Date.now() >= healDeadlineAt) {
|
|
359032
|
+
return {
|
|
359033
|
+
passed: false,
|
|
359034
|
+
verdict: "config_issue",
|
|
359035
|
+
healReason: "mobile heal deadline exceeded before opening the live Phase-2 session"
|
|
359036
|
+
};
|
|
359037
|
+
}
|
|
359038
|
+
log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
|
|
358781
359039
|
try {
|
|
358782
359040
|
await openLiveSession();
|
|
358783
359041
|
const liveDriver = {
|
|
@@ -359623,12 +359881,29 @@ async function startRunner(opts) {
|
|
|
359623
359881
|
}
|
|
359624
359882
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
359625
359883
|
pidsWatchdog.unref();
|
|
359884
|
+
const deferralLogged = /* @__PURE__ */ new Map();
|
|
359885
|
+
const logDeferredOnce = (runId, line) => {
|
|
359886
|
+
if (deferralLogged.get(runId) === line) return;
|
|
359887
|
+
deferralLogged.set(runId, line);
|
|
359888
|
+
log.dim(line);
|
|
359889
|
+
};
|
|
359890
|
+
let lastPendingSummary = "";
|
|
359626
359891
|
while (!shuttingDown) {
|
|
359627
359892
|
try {
|
|
359628
359893
|
if (activeRuns < MAX_CONCURRENT) {
|
|
359629
359894
|
const pending = await fetchPendingRuns(serverUrl, headers);
|
|
359895
|
+
{
|
|
359896
|
+
const pendingIds = new Set(pending.map((r) => r.runId));
|
|
359897
|
+
for (const id of [...deferralLogged.keys()]) {
|
|
359898
|
+
if (!pendingIds.has(id)) deferralLogged.delete(id);
|
|
359899
|
+
}
|
|
359900
|
+
}
|
|
359630
359901
|
if (pending.length > 0) {
|
|
359631
|
-
|
|
359902
|
+
const pendingSummary = `${pending.length} pending run(s) found (${activeRuns}/${MAX_CONCURRENT} slots used)`;
|
|
359903
|
+
if (pendingSummary !== lastPendingSummary) {
|
|
359904
|
+
lastPendingSummary = pendingSummary;
|
|
359905
|
+
log.info(pendingSummary);
|
|
359906
|
+
}
|
|
359632
359907
|
const staggerMs = envInt("RUN_STAGGER_MS", DEFAULT_RUN_STAGGER_MS);
|
|
359633
359908
|
let claimedThisPoll = 0;
|
|
359634
359909
|
for (const run2 of pending) {
|
|
@@ -359641,7 +359916,7 @@ async function startRunner(opts) {
|
|
|
359641
359916
|
const runProject = projectKey(run2);
|
|
359642
359917
|
const projectLimit = projectCapacityFor(runProject, pending);
|
|
359643
359918
|
if (getActiveForProject(runProject) >= projectLimit) {
|
|
359644
|
-
|
|
359919
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
359645
359920
|
continue;
|
|
359646
359921
|
}
|
|
359647
359922
|
const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
@@ -359651,27 +359926,27 @@ async function startRunner(opts) {
|
|
|
359651
359926
|
browserPool: browserPool.stats()
|
|
359652
359927
|
});
|
|
359653
359928
|
if (!admission.allowed) {
|
|
359654
|
-
|
|
359929
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
|
|
359655
359930
|
continue;
|
|
359656
359931
|
}
|
|
359657
359932
|
if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
|
|
359658
|
-
|
|
359933
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
|
|
359659
359934
|
continue;
|
|
359660
359935
|
}
|
|
359661
359936
|
const discoveryMedium = runMode === "discovery" ? run2.discoveryContext?.medium : void 0;
|
|
359662
359937
|
if (discoveryMedium === "IOS_NATIVE" || discoveryMedium === "ANDROID_NATIVE") {
|
|
359663
359938
|
if (!enableMobile) {
|
|
359664
|
-
|
|
359939
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (native mobile discovery needs a runner started with --mobile)`);
|
|
359665
359940
|
continue;
|
|
359666
359941
|
}
|
|
359667
359942
|
const caps = getRunnerCapabilities();
|
|
359668
359943
|
const hasDeviceCap = discoveryMedium === "IOS_NATIVE" ? caps.ios : caps.android;
|
|
359669
359944
|
if (!hasDeviceCap) {
|
|
359670
|
-
|
|
359945
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
|
|
359671
359946
|
continue;
|
|
359672
359947
|
}
|
|
359673
359948
|
if (getBridgeState().hasActiveSession) {
|
|
359674
|
-
|
|
359949
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
|
|
359675
359950
|
continue;
|
|
359676
359951
|
}
|
|
359677
359952
|
}
|
|
@@ -359716,7 +359991,7 @@ async function startRunner(opts) {
|
|
|
359716
359991
|
activeMobileDiscoveries
|
|
359717
359992
|
});
|
|
359718
359993
|
if (!decision.claim) {
|
|
359719
|
-
|
|
359994
|
+
logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
|
|
359720
359995
|
continue;
|
|
359721
359996
|
}
|
|
359722
359997
|
leasedDeviceId = freeDeviceId;
|
package/dist/cli.mjs
CHANGED
|
@@ -2258,7 +2258,8 @@ function selectorCandidatesForStep(step) {
|
|
|
2258
2258
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2259
2259
|
const pushValueCandidates = (value) => {
|
|
2260
2260
|
candidates.push(selectorForValue(value));
|
|
2261
|
-
|
|
2261
|
+
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2262
|
+
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2262
2263
|
candidates.push(`id=${value}`);
|
|
2263
2264
|
}
|
|
2264
2265
|
};
|
|
@@ -7788,12 +7789,16 @@ var require_mobile_keyboard = __commonJS({
|
|
|
7788
7789
|
"use strict";
|
|
7789
7790
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7790
7791
|
exports.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
|
|
7792
|
+
exports.hasFocusedEditableElement = hasFocusedEditableElement;
|
|
7791
7793
|
exports.dismissKeyboardForRetry = dismissKeyboardForRetry;
|
|
7792
7794
|
var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
|
|
7793
7795
|
var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
|
|
7794
7796
|
function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
|
|
7795
7797
|
if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
|
|
7796
7798
|
return false;
|
|
7799
|
+
return hasFocusedEditableElement(elements);
|
|
7800
|
+
}
|
|
7801
|
+
function hasFocusedEditableElement(elements) {
|
|
7797
7802
|
return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
|
|
7798
7803
|
}
|
|
7799
7804
|
async function dismissKeyboardForRetry(driver) {
|
|
@@ -75298,6 +75303,9 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75298
75303
|
let totalBreadthContinuationGranted = 0;
|
|
75299
75304
|
let progressMarkAtLastBreadthContinuation = null;
|
|
75300
75305
|
let lastPostSubmitConfirmationToken = null;
|
|
75306
|
+
let keyboardRecoveryScreenId = null;
|
|
75307
|
+
let keyboardRecoverySameScreenTurns = 0;
|
|
75308
|
+
let lastKeyboardRecoveryIteration = -Infinity;
|
|
75301
75309
|
const maybeGrantBreadthContinuation = (iteration, breadthCoverage) => {
|
|
75302
75310
|
if (mode !== "survey" || breadthCoverage.complete || iteration < Math.floor(maxIterations * 0.8))
|
|
75303
75311
|
return 0;
|
|
@@ -75368,6 +75376,29 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75368
75376
|
log2(` Post-submit confirmation snapshot failed non-fatally: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
75369
75377
|
}
|
|
75370
75378
|
}
|
|
75379
|
+
{
|
|
75380
|
+
const rawScreenId = state2.getCurrentScreenId();
|
|
75381
|
+
const webIdx = rawScreenId ? rawScreenId.indexOf(".web-") : -1;
|
|
75382
|
+
const hashIdx = rawScreenId ? rawScreenId.indexOf("#") : -1;
|
|
75383
|
+
const currentScreenId = rawScreenId && webIdx >= 0 ? `${rawScreenId.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${rawScreenId.slice(webIdx)}` : rawScreenId;
|
|
75384
|
+
if (currentScreenId && currentScreenId === keyboardRecoveryScreenId) {
|
|
75385
|
+
keyboardRecoverySameScreenTurns += 1;
|
|
75386
|
+
} else {
|
|
75387
|
+
keyboardRecoveryScreenId = currentScreenId ?? null;
|
|
75388
|
+
keyboardRecoverySameScreenTurns = 0;
|
|
75389
|
+
}
|
|
75390
|
+
const keyboardLikelyOpen = (0, mobile_keyboard_js_1.hasFocusedEditableElement)(getLatest()?.screen.elements ?? []);
|
|
75391
|
+
if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
|
|
75392
|
+
lastKeyboardRecoveryIteration = iteration;
|
|
75393
|
+
try {
|
|
75394
|
+
await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
75395
|
+
log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen with a focused input).");
|
|
75396
|
+
transientDirectives.push("HARNESS NOTE: the on-screen keyboard was just auto-dismissed because multiple turns stalled on this screen \u2014 controls it occluded (e.g. the form submit button) may now be visible. Call mobile_snapshot, then TAP the commit/submit control instead of re-filling fields.");
|
|
75397
|
+
} catch (error2) {
|
|
75398
|
+
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
|
|
75399
|
+
}
|
|
75400
|
+
}
|
|
75401
|
+
}
|
|
75371
75402
|
if (iteration > 6 && state2.isStaleProgress()) {
|
|
75372
75403
|
transientDirectives.push("You have made no screen-graph progress recently. If the accessible content is covered, call finish_exploration. Otherwise swipe to reveal off-screen content or tap a not-yet-explored element.");
|
|
75373
75404
|
}
|
|
@@ -79950,10 +79981,34 @@ ${constraints.join("\n")}`);
|
|
|
79950
79981
|
const via = triggerLabel ? ` via ${JSON.stringify(triggerLabel)}` : "";
|
|
79951
79982
|
group.flows?.push(`${nameById.get(transition.fromScreenId) ?? transition.fromScreenId} \u2192 ${nameById.get(transition.toScreenId) ?? transition.toScreenId} (${transition.action}${via})`);
|
|
79952
79983
|
}
|
|
79953
|
-
return [...groups.values()].map(({ realm
|
|
79954
|
-
|
|
79955
|
-
|
|
79956
|
-
|
|
79984
|
+
return [...groups.values()].map(({ realm, ...group }) => {
|
|
79985
|
+
const seenPageKeys = /* @__PURE__ */ new Set();
|
|
79986
|
+
const routes = [];
|
|
79987
|
+
const pages = [];
|
|
79988
|
+
group.routes.forEach((route2, index) => {
|
|
79989
|
+
const webIdx = route2.indexOf(".web-");
|
|
79990
|
+
const hashIdx = route2.indexOf("#");
|
|
79991
|
+
const key = webIdx >= 0 ? `${route2.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${route2.slice(webIdx)}` : route2;
|
|
79992
|
+
if (seenPageKeys.has(key))
|
|
79993
|
+
return;
|
|
79994
|
+
seenPageKeys.add(key);
|
|
79995
|
+
routes.push(route2);
|
|
79996
|
+
const page = group.pages?.[index];
|
|
79997
|
+
if (page)
|
|
79998
|
+
pages.push(page);
|
|
79999
|
+
});
|
|
80000
|
+
const flows = [...new Set(group.flows ?? [])];
|
|
80001
|
+
const screenNames = [...new Set(routes.map((route2) => nameById.get(route2) ?? route2))];
|
|
80002
|
+
const preview = screenNames.slice(0, 3).join(", ");
|
|
80003
|
+
const description = `${realm === "authenticated" ? "Authenticated" : "Public"} mobile area "${group.name}" \u2014 ${routes.length} screen(s)${preview ? ` (${preview}${screenNames.length > 3 ? ", \u2026" : ""})` : ""}${flows.length > 0 ? `, ${flows.length} observed transition(s)` : ""}.`;
|
|
80004
|
+
return {
|
|
80005
|
+
...group,
|
|
80006
|
+
routes,
|
|
80007
|
+
pages,
|
|
80008
|
+
description,
|
|
80009
|
+
flows
|
|
80010
|
+
};
|
|
80011
|
+
});
|
|
79957
80012
|
}
|
|
79958
80013
|
function normalizeMobileSurveyFeatureGroups(groups, graph, appId, existingFeatureNames = []) {
|
|
79959
80014
|
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
|
|
@@ -82135,6 +82190,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82135
82190
|
function weakAssertionTargetReason(element) {
|
|
82136
82191
|
if (isAlwaysPresentContainer(element))
|
|
82137
82192
|
return "always-present app/system container";
|
|
82193
|
+
const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
|
|
82194
|
+
if (!hasIdentitySignal)
|
|
82195
|
+
return "no identity locator (accessibility id / resource id / text) for the assert gate";
|
|
82138
82196
|
const structuralIdentity = [
|
|
82139
82197
|
element.accessibilityId,
|
|
82140
82198
|
element.resourceId,
|
|
@@ -83637,6 +83695,94 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
83637
83695
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
83638
83696
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
83639
83697
|
}
|
|
83698
|
+
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
83699
|
+
function isIdentityEqualityXpath(value) {
|
|
83700
|
+
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
83701
|
+
}
|
|
83702
|
+
function stepLacksIdentityAnchor(step) {
|
|
83703
|
+
const metadata = step.metadata;
|
|
83704
|
+
if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
|
|
83705
|
+
return false;
|
|
83706
|
+
const target = step.target;
|
|
83707
|
+
if (!target)
|
|
83708
|
+
return true;
|
|
83709
|
+
const bundle = target.locatorBundle;
|
|
83710
|
+
if (bundle?.accessibilityId || bundle?.resourceId)
|
|
83711
|
+
return false;
|
|
83712
|
+
if (bundle?.text?.trim() || bundle?.contentDesc?.trim())
|
|
83713
|
+
return false;
|
|
83714
|
+
if (bundle?.xpath && isIdentityEqualityXpath(bundle.xpath))
|
|
83715
|
+
return false;
|
|
83716
|
+
for (const raw of [target.primary, ...target.fallbacks ?? []]) {
|
|
83717
|
+
if (typeof raw !== "string")
|
|
83718
|
+
continue;
|
|
83719
|
+
const value = raw.trim();
|
|
83720
|
+
if (!value)
|
|
83721
|
+
continue;
|
|
83722
|
+
if (value.startsWith("css="))
|
|
83723
|
+
continue;
|
|
83724
|
+
if (value.startsWith("~") && value.length > 1)
|
|
83725
|
+
return false;
|
|
83726
|
+
if (value.startsWith("id=") && value.length > 3)
|
|
83727
|
+
return false;
|
|
83728
|
+
if (value.startsWith("//") || value.startsWith("(")) {
|
|
83729
|
+
if (isIdentityEqualityXpath(value))
|
|
83730
|
+
return false;
|
|
83731
|
+
continue;
|
|
83732
|
+
}
|
|
83733
|
+
if (value.startsWith("/"))
|
|
83734
|
+
continue;
|
|
83735
|
+
return false;
|
|
83736
|
+
}
|
|
83737
|
+
return true;
|
|
83738
|
+
}
|
|
83739
|
+
async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
|
|
83740
|
+
if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
|
|
83741
|
+
return null;
|
|
83742
|
+
const identityless = steps.filter(stepLacksIdentityAnchor);
|
|
83743
|
+
if (identityless.length === 0)
|
|
83744
|
+
return null;
|
|
83745
|
+
const isAssert = (step) => /^assert/i.test(step.action);
|
|
83746
|
+
if (!identityless.every(isAssert)) {
|
|
83747
|
+
return {
|
|
83748
|
+
passed: false,
|
|
83749
|
+
verdict: "unhealable_dynamic",
|
|
83750
|
+
healReason: "identity gate: an action step carries no identity locator (accessibility id / resource id / text); the recording lacks the anchors replay requires \u2014 re-record the step"
|
|
83751
|
+
};
|
|
83752
|
+
}
|
|
83753
|
+
const pruned = steps.filter((step) => !stepLacksIdentityAnchor(step));
|
|
83754
|
+
if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
|
|
83755
|
+
return {
|
|
83756
|
+
passed: false,
|
|
83757
|
+
verdict: "unhealable_dynamic",
|
|
83758
|
+
healReason: "identity gate: the only assertion(s) carry no identity locator, so the test cannot prove its outcome as recorded \u2014 re-record with an identifiable anchor"
|
|
83759
|
+
};
|
|
83760
|
+
}
|
|
83761
|
+
const reindexed = pruned.map((step, order) => ({ ...step, order }));
|
|
83762
|
+
try {
|
|
83763
|
+
const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
|
|
83764
|
+
if (!verify.passed)
|
|
83765
|
+
return null;
|
|
83766
|
+
let healedAppiumCode;
|
|
83767
|
+
if (ctx.testName) {
|
|
83768
|
+
try {
|
|
83769
|
+
healedAppiumCode = (0, shared_1.buildAppiumCode)(ctx.testName, toDeterministicTarget(target), reindexed);
|
|
83770
|
+
} catch {
|
|
83771
|
+
}
|
|
83772
|
+
}
|
|
83773
|
+
return {
|
|
83774
|
+
passed: true,
|
|
83775
|
+
verdict: "fix",
|
|
83776
|
+
healedSteps: reindexed,
|
|
83777
|
+
...healedAppiumCode ? { healedAppiumCode } : {},
|
|
83778
|
+
healReason: `identity gate: dropped ${identityless.length} assert step(s) recorded without any identity locator; pruned test verified green in one replay`,
|
|
83779
|
+
stepResults: verify.stepResults,
|
|
83780
|
+
logs: verify.logs
|
|
83781
|
+
};
|
|
83782
|
+
} catch {
|
|
83783
|
+
return null;
|
|
83784
|
+
}
|
|
83785
|
+
}
|
|
83640
83786
|
function stripCodeFences(text) {
|
|
83641
83787
|
const trimmed = text.trim();
|
|
83642
83788
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -83977,7 +84123,17 @@ ${args.directive}`);
|
|
|
83977
84123
|
break;
|
|
83978
84124
|
}
|
|
83979
84125
|
}
|
|
84126
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84127
|
+
return {
|
|
84128
|
+
passed: false,
|
|
84129
|
+
verdict: "config_issue",
|
|
84130
|
+
healReason: `mobile heal deadline exceeded during Phase 1 (after ${attempt} attempt(s)) \u2014 aborting before further model turns`,
|
|
84131
|
+
attemptsUsed: attempt,
|
|
84132
|
+
lastError
|
|
84133
|
+
};
|
|
84134
|
+
}
|
|
83980
84135
|
attempt += 1;
|
|
84136
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}/${attemptCap} \u2014 asking the model for a script fix...`);
|
|
83981
84137
|
const userContent = buildUserContent({
|
|
83982
84138
|
attempt,
|
|
83983
84139
|
baseSteps,
|
|
@@ -84004,6 +84160,7 @@ ${args.directive}`);
|
|
|
84004
84160
|
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
84005
84161
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84006
84162
|
} catch {
|
|
84163
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}: model call failed in transport \u2014 granting a recovery turn`);
|
|
84007
84164
|
markRecoverableTurn();
|
|
84008
84165
|
pendingDirective = "The previous attempt did not reach the model. Re-analyze and return one valid JSON verdict.";
|
|
84009
84166
|
continue;
|
|
@@ -84178,6 +84335,9 @@ ${args.directive}`);
|
|
|
84178
84335
|
const preflight = await tryRegistryPreflight(steps, target, ctx, options);
|
|
84179
84336
|
if (preflight)
|
|
84180
84337
|
return preflight;
|
|
84338
|
+
const identityRepair = await tryIdentityGateStaticRepair(steps, target, ctx, options);
|
|
84339
|
+
if (identityRepair)
|
|
84340
|
+
return identityRepair;
|
|
84181
84341
|
const script = await executeMobileScriptDrivenHeal(steps, target, ctx, options);
|
|
84182
84342
|
if (script.providerUnavailable) {
|
|
84183
84343
|
const fallback = await tryDeterministicProviderFallback({ steps, target, ctx, options });
|
|
@@ -84351,6 +84511,14 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
84351
84511
|
let bestStepResults;
|
|
84352
84512
|
let aiResponseCount = 0;
|
|
84353
84513
|
for (let pass = 1; pass <= maxPasses; pass += 1) {
|
|
84514
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84515
|
+
return {
|
|
84516
|
+
passed: false,
|
|
84517
|
+
verdict: "config_issue",
|
|
84518
|
+
healReason: `mobile heal deadline exceeded during live (Phase 2) healing at pass ${pass}/${maxPasses}`
|
|
84519
|
+
};
|
|
84520
|
+
}
|
|
84521
|
+
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
84354
84522
|
let snap;
|
|
84355
84523
|
try {
|
|
84356
84524
|
snap = await liveDriver.snapshot();
|
|
@@ -319095,7 +319263,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319095
319263
|
"../runner-core/dist/services/survey-link-frontier.js"(exports) {
|
|
319096
319264
|
"use strict";
|
|
319097
319265
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
319098
|
-
exports.FINISH_GATE_MIN_BUDGET_PCT = exports.MAX_FINISH_REDIRECTS = void 0;
|
|
319266
|
+
exports.FINISH_ESCAPE_BOUNCE_STREAK = exports.FINISH_GATE_MIN_BUDGET_PCT = exports.MAX_FINISH_REDIRECTS = void 0;
|
|
319267
|
+
exports.initialFinishBounceState = initialFinishBounceState;
|
|
319268
|
+
exports.evaluateFinishBounce = evaluateFinishBounce;
|
|
319269
|
+
exports.recordFinishBounce = recordFinishBounce;
|
|
319099
319270
|
exports.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
|
|
319100
319271
|
exports.extractSnapshotLinks = extractSnapshotLinks;
|
|
319101
319272
|
exports.deriveLinkScope = deriveLinkScope;
|
|
@@ -319112,6 +319283,17 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319112
319283
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
319113
319284
|
exports.MAX_FINISH_REDIRECTS = 2;
|
|
319114
319285
|
exports.FINISH_GATE_MIN_BUDGET_PCT = 0.2;
|
|
319286
|
+
exports.FINISH_ESCAPE_BOUNCE_STREAK = 3;
|
|
319287
|
+
function initialFinishBounceState() {
|
|
319288
|
+
return { streak: 0, pagesBaseline: 0 };
|
|
319289
|
+
}
|
|
319290
|
+
function evaluateFinishBounce(prev, pagesVisited, threshold = exports.FINISH_ESCAPE_BOUNCE_STREAK) {
|
|
319291
|
+
const base2 = pagesVisited > prev.pagesBaseline ? { streak: 0, pagesBaseline: pagesVisited } : prev;
|
|
319292
|
+
return { state: base2, escape: base2.streak >= threshold };
|
|
319293
|
+
}
|
|
319294
|
+
function recordFinishBounce(prev) {
|
|
319295
|
+
return { streak: prev.streak + 1, pagesBaseline: prev.pagesBaseline };
|
|
319296
|
+
}
|
|
319115
319297
|
function isSurveyLinkFrontierEnabled() {
|
|
319116
319298
|
const v = process.env.DISCOVERY_SURVEY_LINK_FRONTIER;
|
|
319117
319299
|
return v === "true" || v === "1";
|
|
@@ -323300,6 +323482,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
323300
323482
|
const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
|
|
323301
323483
|
const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
|
|
323302
323484
|
let finishRedirects = 0;
|
|
323485
|
+
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
323303
323486
|
let lastLandedPageUrl = null;
|
|
323304
323487
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
323305
323488
|
function ingestLinksFromText(rawText) {
|
|
@@ -323537,12 +323720,14 @@ ${inboxPromptBlock}`;
|
|
|
323537
323720
|
const item = state2.frontier.get(r);
|
|
323538
323721
|
return item?.source === "link-seen" && item.attempted === true;
|
|
323539
323722
|
};
|
|
323540
|
-
const
|
|
323723
|
+
const isSatisfied = (r) => (0, survey_link_frontier_js_1.visitedSatisfies)(state2, r);
|
|
323724
|
+
const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r) && !isSatisfied(r));
|
|
323541
323725
|
const rankedSet = new Set(ranked);
|
|
323542
323726
|
const navigatedUncaptured = [];
|
|
323543
323727
|
for (const r of state2.frontier.keys()) {
|
|
323544
|
-
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
|
|
323728
|
+
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r) && !isSatisfied(r)) {
|
|
323545
323729
|
navigatedUncaptured.push(r);
|
|
323730
|
+
}
|
|
323546
323731
|
}
|
|
323547
323732
|
return ranked.concat(navigatedUncaptured);
|
|
323548
323733
|
};
|
|
@@ -324095,6 +324280,7 @@ ${currentSummary}`;
|
|
|
324095
324280
|
}
|
|
324096
324281
|
const toolResults = [];
|
|
324097
324282
|
let finishBouncedThisBatch = false;
|
|
324283
|
+
let finishStreakCountedThisBatch = false;
|
|
324098
324284
|
const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
324099
324285
|
const { secrets: secretsOnlyForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
|
|
324100
324286
|
const allowedCredentialValues = inboxState ? [inboxState.email] : [];
|
|
@@ -324273,25 +324459,41 @@ ${currentSummary}`;
|
|
|
324273
324459
|
}
|
|
324274
324460
|
logs2.push(` Captured page: ${normalized.route}, ${normalized.provenCommands.length} proven commands${normalized.alreadyCaptured ? " (RE-VISIT)" : ""}`);
|
|
324275
324461
|
const transitionHint = !normalized.alreadyCaptured && isSurveyMode ? { tip: `Also call record_transition with fromRoute, toRoute, triggerType, and triggerLabel (the exact link text you clicked to get here).` } : {};
|
|
324276
|
-
const
|
|
324277
|
-
|
|
324278
|
-
|
|
324279
|
-
{
|
|
324280
|
-
|
|
324281
|
-
|
|
324282
|
-
|
|
324283
|
-
|
|
324284
|
-
|
|
324285
|
-
|
|
324286
|
-
|
|
324287
|
-
|
|
324288
|
-
|
|
324289
|
-
|
|
324290
|
-
|
|
324291
|
-
|
|
324292
|
-
|
|
324293
|
-
|
|
324294
|
-
|
|
324462
|
+
const progressLine = `${state2.pagesVisited} pages, ${state2.transitionsFound} transitions, ${state2.recordedJourneys.length} tests | iteration ${iteration}/${maxIterations} (${maxIterations - iteration} remaining)`;
|
|
324463
|
+
if (isSurveyMode && normalized.alreadyCaptured && normalized.observations.length === 0 && (adaptiveActive || linkFrontierEnabled)) {
|
|
324464
|
+
const nextRoutes = (adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route)).slice(0, 6);
|
|
324465
|
+
const steer = nextRoutes.length > 0 ? `Already mapped \u2014 no new intel on this page. Still un-captured: ${nextRoutes.join(", ")}. Navigate to one and capture_page, or call finish_exploration if none remain.` : "Already mapped and no un-captured routes remain. Call finish_exploration.";
|
|
324466
|
+
toolResults.push({
|
|
324467
|
+
role: "tool",
|
|
324468
|
+
tool_call_id: toolCall.id,
|
|
324469
|
+
content: JSON.stringify({
|
|
324470
|
+
acknowledged: true,
|
|
324471
|
+
alreadyCaptured: true,
|
|
324472
|
+
note: steer,
|
|
324473
|
+
progress: progressLine
|
|
324474
|
+
})
|
|
324475
|
+
});
|
|
324476
|
+
} else {
|
|
324477
|
+
const lastAction = normalized.observations.length > 0 ? normalized.observations[normalized.observations.length - 1].action : void 0;
|
|
324478
|
+
const snapshotBlock = (0, snapshot_observation_js_1.renderCompactSnapshotBlock)(
|
|
324479
|
+
normalized,
|
|
324480
|
+
{ lastAction },
|
|
324481
|
+
// G10: no explicit elementLimit — let the flag-aware deep default govern
|
|
324482
|
+
// (flags-off 6 = byte-identical; richRender 8, so demoted form controls fit).
|
|
324483
|
+
{ formLimit: 3, observationLimit: 3, mode: options?.mode ?? "full", recoveredClickables }
|
|
324484
|
+
);
|
|
324485
|
+
toolResults.push({
|
|
324486
|
+
role: "tool",
|
|
324487
|
+
tool_call_id: toolCall.id,
|
|
324488
|
+
content: JSON.stringify({
|
|
324489
|
+
acknowledged: true,
|
|
324490
|
+
...normalized.alreadyCaptured ? { note: "Already captured - move on to unvisited pages." } : {},
|
|
324491
|
+
...transitionHint,
|
|
324492
|
+
snapshot: snapshotBlock,
|
|
324493
|
+
progress: progressLine
|
|
324494
|
+
})
|
|
324495
|
+
});
|
|
324496
|
+
}
|
|
324295
324497
|
} catch (err) {
|
|
324296
324498
|
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
324297
324499
|
logs2.push(` Error persisting page: ${errMsg2}`);
|
|
@@ -324708,7 +324910,18 @@ ${currentSummary}`;
|
|
|
324708
324910
|
continue;
|
|
324709
324911
|
}
|
|
324710
324912
|
if (toolName === "finish_exploration") {
|
|
324711
|
-
|
|
324913
|
+
const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
|
|
324914
|
+
const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
|
|
324915
|
+
finishBounce = fb.state;
|
|
324916
|
+
const forceAcceptFinish = fb.escape;
|
|
324917
|
+
if (forceAcceptFinish) {
|
|
324918
|
+
const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
|
|
324919
|
+
const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
324920
|
+
logs2.push(` [finish-escape] accepting finish after ${finishBounce.streak} consecutive no-progress bounce(s)` + (stuck.length > 0 ? `; ${stuck.length} route(s) unresolved: ${stuck.slice(0, 6).join(", ")}${stuck.length > 6 ? " \u2026" : ""}` : ""));
|
|
324921
|
+
if (reasonText)
|
|
324922
|
+
logs2.push(` [finish-escape] agent's stated reason: ${reasonText}`);
|
|
324923
|
+
}
|
|
324924
|
+
if (!forceAcceptFinish && linkFrontierEnabled) {
|
|
324712
324925
|
const blocking = (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2);
|
|
324713
324926
|
const budgetRemainingPct = maxIterations > 0 ? (maxIterations - iteration) / maxIterations : 0;
|
|
324714
324927
|
if (blocking.length > 0 && budgetRemainingPct >= survey_link_frontier_js_1.FINISH_GATE_MIN_BUDGET_PCT && (finishRedirects < survey_link_frontier_js_1.MAX_FINISH_REDIRECTS || finishBouncedThisBatch)) {
|
|
@@ -324727,13 +324940,17 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324727
324940
|
reason: `Not done yet - ${blocking.length} route(s) were visible as links but never visited or captured: ${targets}. A survey must map every reachable top-level page. Navigate to each, capture_page + record_transition, then call finish_exploration. If one is genuinely unreachable (requires credentials you don't have, or leaves the app), note that in your summary and move on.`
|
|
324728
324941
|
})
|
|
324729
324942
|
});
|
|
324943
|
+
if (!finishStreakCountedThisBatch) {
|
|
324944
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
324945
|
+
finishStreakCountedThisBatch = true;
|
|
324946
|
+
}
|
|
324730
324947
|
continue;
|
|
324731
324948
|
}
|
|
324732
324949
|
if (blocking.length > 0) {
|
|
324733
324950
|
logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
|
|
324734
324951
|
}
|
|
324735
324952
|
}
|
|
324736
|
-
if (adaptiveActive && !adaptiveComplete) {
|
|
324953
|
+
if (!forceAcceptFinish && adaptiveActive && !adaptiveComplete) {
|
|
324737
324954
|
refreshAdaptiveDiscovery();
|
|
324738
324955
|
const remainingList = adaptiveDebtRoutes();
|
|
324739
324956
|
if (remainingList.length > 0 && iteration < adaptiveHardCap) {
|
|
@@ -324741,10 +324958,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324741
324958
|
if (iteration >= adaptiveDecisionArmedAt) {
|
|
324742
324959
|
const { directive, keepGoing } = runBudgetController();
|
|
324743
324960
|
if (keepGoing) {
|
|
324744
|
-
reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
|
|
324961
|
+
reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
|
|
324745
324962
|
}
|
|
324746
324963
|
} else {
|
|
324747
|
-
reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
|
|
324964
|
+
reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
|
|
324748
324965
|
}
|
|
324749
324966
|
if (reason) {
|
|
324750
324967
|
toolResults.push({
|
|
@@ -324753,6 +324970,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324753
324970
|
content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
|
|
324754
324971
|
});
|
|
324755
324972
|
logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
|
|
324973
|
+
if (!finishStreakCountedThisBatch) {
|
|
324974
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
324975
|
+
finishStreakCountedThisBatch = true;
|
|
324976
|
+
}
|
|
324756
324977
|
continue;
|
|
324757
324978
|
}
|
|
324758
324979
|
}
|
|
@@ -349380,7 +349601,7 @@ var require_package4 = __commonJS({
|
|
|
349380
349601
|
"package.json"(exports, module) {
|
|
349381
349602
|
module.exports = {
|
|
349382
349603
|
name: "@validate.qa/runner",
|
|
349383
|
-
version: "1.0.
|
|
349604
|
+
version: "1.0.56",
|
|
349384
349605
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
349385
349606
|
bin: {
|
|
349386
349607
|
"validate-runner": "dist/cli.js"
|
|
@@ -350994,6 +351215,17 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
350994
351215
|
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
350995
351216
|
throw new Error(`WEBVIEW element not found for css=${css}`);
|
|
350996
351217
|
}
|
|
351218
|
+
try {
|
|
351219
|
+
const scriptResult = await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
351220
|
+
method: "POST",
|
|
351221
|
+
body: JSON.stringify({
|
|
351222
|
+
script: "var el = arguments[0];var form = el && (el.form || (el.closest ? el.closest('form') : null));if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); return 'request-submit'; }return 'no-form';",
|
|
351223
|
+
args: [{ ELEMENT: elementId, "element-6066-11e4-a52e-4f735466cecf": elementId }]
|
|
351224
|
+
})
|
|
351225
|
+
});
|
|
351226
|
+
if (readValue(scriptResult) === "request-submit") return;
|
|
351227
|
+
} catch {
|
|
351228
|
+
}
|
|
350997
351229
|
await appiumRequest(`${sessionPath}/element/${elementId}/click`, {
|
|
350998
351230
|
method: "POST",
|
|
350999
351231
|
body: "{}"
|
|
@@ -351003,6 +351235,14 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
351003
351235
|
body: JSON.stringify({ text: WEBDRIVER_ENTER_KEY, value: [WEBDRIVER_ENTER_KEY] })
|
|
351004
351236
|
});
|
|
351005
351237
|
}
|
|
351238
|
+
async function hideKeyboardIfShown(sessionPath) {
|
|
351239
|
+
try {
|
|
351240
|
+
const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
|
|
351241
|
+
if (readValue(shown) !== true) return;
|
|
351242
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, { method: "POST", body: "{}" });
|
|
351243
|
+
} catch {
|
|
351244
|
+
}
|
|
351245
|
+
}
|
|
351006
351246
|
async function withWebViewContext(sessionPath, webviewName, body, options = {}) {
|
|
351007
351247
|
const previous = await getAppContext(sessionPath) ?? "NATIVE_APP";
|
|
351008
351248
|
let bodyCompleted = false;
|
|
@@ -355807,6 +356047,7 @@ var MobileBridgeDriver = class {
|
|
|
355807
356047
|
() => typeCssInCurrentContext(this.sessionPath, css, value),
|
|
355808
356048
|
this.webViewEntryOptions(webviewContext)
|
|
355809
356049
|
);
|
|
356050
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355810
356051
|
});
|
|
355811
356052
|
}
|
|
355812
356053
|
async webSubmitKeyboard(webviewContext, css) {
|
|
@@ -355817,6 +356058,7 @@ var MobileBridgeDriver = class {
|
|
|
355817
356058
|
() => submitCssKeyboardInCurrentContext(this.sessionPath, css),
|
|
355818
356059
|
this.webViewEntryOptions(webviewContext)
|
|
355819
356060
|
);
|
|
356061
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355820
356062
|
});
|
|
355821
356063
|
}
|
|
355822
356064
|
};
|
|
@@ -356256,7 +356498,7 @@ var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bea
|
|
|
356256
356498
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
356257
356499
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
356258
356500
|
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
356259
|
-
var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION;
|
|
356501
|
+
var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION ?? (RUNNER_VERSION !== "0.0.0" ? `npm-v${RUNNER_VERSION}` : void 0);
|
|
356260
356502
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
356261
356503
|
var DEFAULT_TYPE_LIMITS = {
|
|
356262
356504
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -356907,7 +357149,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
356907
357149
|
}, assignedDeviceId, mobileLeaseError) {
|
|
356908
357150
|
const runMode = run2.mode ?? "run";
|
|
356909
357151
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
356910
|
-
|
|
357152
|
+
const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
|
|
357153
|
+
const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
|
|
357154
|
+
log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
|
|
356911
357155
|
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
356912
357156
|
sessionId: `${run2.runId}`,
|
|
356913
357157
|
mode: toLiveBrowserMode(runMode),
|
|
@@ -358741,9 +358985,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358741
358985
|
}
|
|
358742
358986
|
const reproduceError = reproduce.error;
|
|
358743
358987
|
const modelOverride = run2.healerModel ?? void 0;
|
|
358988
|
+
const healDeadlineAt = Date.now() + Math.max(6e4, envInt("MOBILE_HEAL_DEADLINE_MS", 20 * 6e4));
|
|
358744
358989
|
const healerOptions = {
|
|
358745
358990
|
...modelOverride ? { model: modelOverride } : {},
|
|
358991
|
+
deadlineAt: healDeadlineAt,
|
|
358746
358992
|
runNativeTest,
|
|
358993
|
+
// Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
|
|
358994
|
+
// for many minutes of model calls with zero device traffic, which
|
|
358995
|
+
// previously printed nothing between the start banner and the verdict.
|
|
358996
|
+
onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
|
|
358747
358997
|
selectorRegistryContext: run2.selectorRegistryContext ?? null,
|
|
358748
358998
|
runLiveHeal: async (a) => {
|
|
358749
358999
|
let liveSession = null;
|
|
@@ -358786,6 +359036,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358786
359036
|
}
|
|
358787
359037
|
return bridgeDriver;
|
|
358788
359038
|
};
|
|
359039
|
+
if (Date.now() >= healDeadlineAt) {
|
|
359040
|
+
return {
|
|
359041
|
+
passed: false,
|
|
359042
|
+
verdict: "config_issue",
|
|
359043
|
+
healReason: "mobile heal deadline exceeded before opening the live Phase-2 session"
|
|
359044
|
+
};
|
|
359045
|
+
}
|
|
359046
|
+
log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
|
|
358789
359047
|
try {
|
|
358790
359048
|
await openLiveSession();
|
|
358791
359049
|
const liveDriver = {
|
|
@@ -359631,12 +359889,29 @@ async function startRunner(opts) {
|
|
|
359631
359889
|
}
|
|
359632
359890
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
359633
359891
|
pidsWatchdog.unref();
|
|
359892
|
+
const deferralLogged = /* @__PURE__ */ new Map();
|
|
359893
|
+
const logDeferredOnce = (runId, line) => {
|
|
359894
|
+
if (deferralLogged.get(runId) === line) return;
|
|
359895
|
+
deferralLogged.set(runId, line);
|
|
359896
|
+
log.dim(line);
|
|
359897
|
+
};
|
|
359898
|
+
let lastPendingSummary = "";
|
|
359634
359899
|
while (!shuttingDown) {
|
|
359635
359900
|
try {
|
|
359636
359901
|
if (activeRuns < MAX_CONCURRENT) {
|
|
359637
359902
|
const pending = await fetchPendingRuns(serverUrl, headers);
|
|
359903
|
+
{
|
|
359904
|
+
const pendingIds = new Set(pending.map((r) => r.runId));
|
|
359905
|
+
for (const id of [...deferralLogged.keys()]) {
|
|
359906
|
+
if (!pendingIds.has(id)) deferralLogged.delete(id);
|
|
359907
|
+
}
|
|
359908
|
+
}
|
|
359638
359909
|
if (pending.length > 0) {
|
|
359639
|
-
|
|
359910
|
+
const pendingSummary = `${pending.length} pending run(s) found (${activeRuns}/${MAX_CONCURRENT} slots used)`;
|
|
359911
|
+
if (pendingSummary !== lastPendingSummary) {
|
|
359912
|
+
lastPendingSummary = pendingSummary;
|
|
359913
|
+
log.info(pendingSummary);
|
|
359914
|
+
}
|
|
359640
359915
|
const staggerMs = envInt("RUN_STAGGER_MS", DEFAULT_RUN_STAGGER_MS);
|
|
359641
359916
|
let claimedThisPoll = 0;
|
|
359642
359917
|
for (const run2 of pending) {
|
|
@@ -359649,7 +359924,7 @@ async function startRunner(opts) {
|
|
|
359649
359924
|
const runProject = projectKey(run2);
|
|
359650
359925
|
const projectLimit = projectCapacityFor(runProject, pending);
|
|
359651
359926
|
if (getActiveForProject(runProject) >= projectLimit) {
|
|
359652
|
-
|
|
359927
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
359653
359928
|
continue;
|
|
359654
359929
|
}
|
|
359655
359930
|
const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
@@ -359659,27 +359934,27 @@ async function startRunner(opts) {
|
|
|
359659
359934
|
browserPool: browserPool.stats()
|
|
359660
359935
|
});
|
|
359661
359936
|
if (!admission.allowed) {
|
|
359662
|
-
|
|
359937
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
|
|
359663
359938
|
continue;
|
|
359664
359939
|
}
|
|
359665
359940
|
if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
|
|
359666
|
-
|
|
359941
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
|
|
359667
359942
|
continue;
|
|
359668
359943
|
}
|
|
359669
359944
|
const discoveryMedium = runMode === "discovery" ? run2.discoveryContext?.medium : void 0;
|
|
359670
359945
|
if (discoveryMedium === "IOS_NATIVE" || discoveryMedium === "ANDROID_NATIVE") {
|
|
359671
359946
|
if (!enableMobile) {
|
|
359672
|
-
|
|
359947
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (native mobile discovery needs a runner started with --mobile)`);
|
|
359673
359948
|
continue;
|
|
359674
359949
|
}
|
|
359675
359950
|
const caps = getRunnerCapabilities();
|
|
359676
359951
|
const hasDeviceCap = discoveryMedium === "IOS_NATIVE" ? caps.ios : caps.android;
|
|
359677
359952
|
if (!hasDeviceCap) {
|
|
359678
|
-
|
|
359953
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
|
|
359679
359954
|
continue;
|
|
359680
359955
|
}
|
|
359681
359956
|
if (getBridgeState().hasActiveSession) {
|
|
359682
|
-
|
|
359957
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
|
|
359683
359958
|
continue;
|
|
359684
359959
|
}
|
|
359685
359960
|
}
|
|
@@ -359724,7 +359999,7 @@ async function startRunner(opts) {
|
|
|
359724
359999
|
activeMobileDiscoveries
|
|
359725
360000
|
});
|
|
359726
360001
|
if (!decision.claim) {
|
|
359727
|
-
|
|
360002
|
+
logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
|
|
359728
360003
|
continue;
|
|
359729
360004
|
}
|
|
359730
360005
|
leasedDeviceId = freeDeviceId;
|
package/package.json
CHANGED