@validate.qa/runner 1.0.38 → 1.0.39
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 +845 -679
- package/dist/cli.mjs +845 -679
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6213,6 +6213,689 @@ var require_mobile_observation = __commonJS({
|
|
|
6213
6213
|
}
|
|
6214
6214
|
});
|
|
6215
6215
|
|
|
6216
|
+
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
6217
|
+
var require_mobile_exploration_state = __commonJS({
|
|
6218
|
+
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
|
|
6219
|
+
"use strict";
|
|
6220
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
6221
|
+
exports2.MobileExplorationState = void 0;
|
|
6222
|
+
exports2.isDestructiveActionLabel = isDestructiveActionLabel;
|
|
6223
|
+
exports2.isDestructiveConfirmationInput = isDestructiveConfirmationInput;
|
|
6224
|
+
exports2.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
6225
|
+
exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
6226
|
+
exports2.deriveScreenId = deriveScreenId;
|
|
6227
|
+
var mobile_observation_js_1 = require_mobile_observation();
|
|
6228
|
+
var DEFAULT_STALE_THRESHOLD = 4;
|
|
6229
|
+
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
6230
|
+
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
6231
|
+
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
6232
|
+
var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
|
|
6233
|
+
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
6234
|
+
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
6235
|
+
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
6236
|
+
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
6237
|
+
var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
|
|
6238
|
+
var DESTRUCTIVE_ACTION_LABEL_RE = /\b(?:deactivate|delete\s+account|delete\s+profile|close\s+account|cancel\s+account|remove\s+account|delete\s+(?:my\s+)?(?:data|history|everything|account)|delete\s+(?:all|forever|permanently)|permanently\s+(?:delete|remove|erase|deactivate)|erase\s+(?:all|everything|account|data)|wipe\s+(?:account|data|everything|all|device)|purge\s+(?:account|data|everything|all)|destroy\s+(?:account|data)|factory\s+reset|reset\s+(?:all\s+data|everything))\b/i;
|
|
6239
|
+
var DESTRUCTIVE_CONFIRMATION_INPUT_RE = /^(?:delete|delete\s+my\s+account|delete\s+account|delete\s+forever|permanently\s+delete|confirm\s+delete|erase|erase\s+all|wipe|deactivate|remove\s+account|close\s+account|yes,?\s*delete(?:\s+my\s+account)?)$/i;
|
|
6240
|
+
function isDestructiveActionLabel(label) {
|
|
6241
|
+
if (!label)
|
|
6242
|
+
return false;
|
|
6243
|
+
return DESTRUCTIVE_ACTION_LABEL_RE.test(normalizeControlIdentityPart(label));
|
|
6244
|
+
}
|
|
6245
|
+
function isDestructiveConfirmationInput(value) {
|
|
6246
|
+
if (!value)
|
|
6247
|
+
return false;
|
|
6248
|
+
return DESTRUCTIVE_CONFIRMATION_INPUT_RE.test(value.trim());
|
|
6249
|
+
}
|
|
6250
|
+
function normalizeControlIdentityPart(value) {
|
|
6251
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
6252
|
+
}
|
|
6253
|
+
function semanticControlAlias(label) {
|
|
6254
|
+
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
6255
|
+
}
|
|
6256
|
+
function localControlCoverageIdentity(screenId, identity) {
|
|
6257
|
+
return `screen:${screenId}:${identity}`;
|
|
6258
|
+
}
|
|
6259
|
+
function storedMobileControlIdentity(candidate) {
|
|
6260
|
+
return [
|
|
6261
|
+
candidate.identity,
|
|
6262
|
+
`kind:${candidate.kind}`,
|
|
6263
|
+
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
6264
|
+
].join(":");
|
|
6265
|
+
}
|
|
6266
|
+
function isSystemBackNavigationControl(element, label) {
|
|
6267
|
+
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
6268
|
+
return true;
|
|
6269
|
+
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
6270
|
+
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
6271
|
+
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
function stableMobileControlIdentity(element) {
|
|
6275
|
+
const role = normalizeControlIdentityPart(element.role || "control");
|
|
6276
|
+
const accessibilityId = element.accessibilityId?.trim();
|
|
6277
|
+
if (accessibilityId)
|
|
6278
|
+
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
6279
|
+
const resourceId = element.resourceId?.trim();
|
|
6280
|
+
if (resourceId)
|
|
6281
|
+
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
6282
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6283
|
+
if (label)
|
|
6284
|
+
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
6285
|
+
return null;
|
|
6286
|
+
}
|
|
6287
|
+
function classifySafeMobileNavigationControl(element) {
|
|
6288
|
+
if (!element.actionable || element.enabled === false)
|
|
6289
|
+
return null;
|
|
6290
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6291
|
+
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
6292
|
+
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
6293
|
+
return null;
|
|
6294
|
+
if (isSystemBackNavigationControl(element, label))
|
|
6295
|
+
return null;
|
|
6296
|
+
const role = element.role.trim();
|
|
6297
|
+
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
6298
|
+
return null;
|
|
6299
|
+
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
6300
|
+
let kind = null;
|
|
6301
|
+
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
6302
|
+
kind = "tab";
|
|
6303
|
+
else if (/menuitem/i.test(role))
|
|
6304
|
+
kind = "menu-item";
|
|
6305
|
+
else if (linkLike && element.navigationProvenance)
|
|
6306
|
+
kind = "link";
|
|
6307
|
+
else if (linkLike)
|
|
6308
|
+
return null;
|
|
6309
|
+
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
6310
|
+
kind = "menu-trigger";
|
|
6311
|
+
else if (element.expanded === false)
|
|
6312
|
+
kind = "disclosure";
|
|
6313
|
+
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
6314
|
+
kind = "menu-trigger";
|
|
6315
|
+
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
6316
|
+
kind = "modal-trigger";
|
|
6317
|
+
else if (element.navigationProvenance)
|
|
6318
|
+
kind = "navigation-button";
|
|
6319
|
+
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
6320
|
+
kind = "navigation-button";
|
|
6321
|
+
if (!kind)
|
|
6322
|
+
return null;
|
|
6323
|
+
const identity = stableMobileControlIdentity(element);
|
|
6324
|
+
return identity ? { identity, label, kind } : null;
|
|
6325
|
+
}
|
|
6326
|
+
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
6327
|
+
if (!element.navigationProvenance)
|
|
6328
|
+
return void 0;
|
|
6329
|
+
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
6330
|
+
if (!durableId && !semanticFamilyId)
|
|
6331
|
+
return void 0;
|
|
6332
|
+
return [
|
|
6333
|
+
"shared-navigation",
|
|
6334
|
+
element.navigationProvenance,
|
|
6335
|
+
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
6336
|
+
candidate.kind,
|
|
6337
|
+
durableId || "semantic-only",
|
|
6338
|
+
normalizeControlIdentityPart(candidate.label)
|
|
6339
|
+
].filter(Boolean).join(":");
|
|
6340
|
+
}
|
|
6341
|
+
function mobileNavigationRegion(element, windowSize) {
|
|
6342
|
+
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
6343
|
+
return "unknown";
|
|
6344
|
+
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
6345
|
+
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
6346
|
+
if (centerY >= 0.72)
|
|
6347
|
+
return "bottom";
|
|
6348
|
+
if (centerY <= 0.28)
|
|
6349
|
+
return "top";
|
|
6350
|
+
if (centerX <= 0.35)
|
|
6351
|
+
return "left";
|
|
6352
|
+
if (centerX >= 0.65)
|
|
6353
|
+
return "right";
|
|
6354
|
+
return "center";
|
|
6355
|
+
}
|
|
6356
|
+
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
6357
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6358
|
+
for (const element of elements) {
|
|
6359
|
+
if (!element.navigationProvenance)
|
|
6360
|
+
continue;
|
|
6361
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6362
|
+
if (!candidate)
|
|
6363
|
+
continue;
|
|
6364
|
+
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
6365
|
+
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
6366
|
+
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
6367
|
+
groups.set(groupKey, labels);
|
|
6368
|
+
}
|
|
6369
|
+
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
6370
|
+
}
|
|
6371
|
+
function deriveScreenId(signal) {
|
|
6372
|
+
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
6373
|
+
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
6374
|
+
}
|
|
6375
|
+
function observedElementIdentity(element) {
|
|
6376
|
+
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
6377
|
+
}
|
|
6378
|
+
function mergeObservedElements(previous, current) {
|
|
6379
|
+
const merged = [...current];
|
|
6380
|
+
const seen = new Set(current.map(observedElementIdentity));
|
|
6381
|
+
for (const element of previous) {
|
|
6382
|
+
const identity = observedElementIdentity(element);
|
|
6383
|
+
if (seen.has(identity))
|
|
6384
|
+
continue;
|
|
6385
|
+
seen.add(identity);
|
|
6386
|
+
merged.push(element);
|
|
6387
|
+
}
|
|
6388
|
+
return merged;
|
|
6389
|
+
}
|
|
6390
|
+
function sanitizeElementsForEvidence(elements) {
|
|
6391
|
+
return elements.map((element) => {
|
|
6392
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
6393
|
+
return {
|
|
6394
|
+
...element,
|
|
6395
|
+
...label ? { label } : { label: void 0 },
|
|
6396
|
+
text: void 0
|
|
6397
|
+
};
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6400
|
+
var MobileExplorationState = class {
|
|
6401
|
+
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
6402
|
+
screens = /* @__PURE__ */ new Map();
|
|
6403
|
+
/** Observed directed transitions between screens. */
|
|
6404
|
+
transitions = [];
|
|
6405
|
+
/** Transition-announced screens that do not yet have a live observation. */
|
|
6406
|
+
frontier = /* @__PURE__ */ new Map();
|
|
6407
|
+
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
6408
|
+
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
6409
|
+
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
6410
|
+
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
6411
|
+
successfulSafeControlAttemptCount = 0;
|
|
6412
|
+
/** Repeated id-less structural navigation families, grouped by region. */
|
|
6413
|
+
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
6414
|
+
nextSemanticNavigationFamilyId = 1;
|
|
6415
|
+
/** The screen-id of the most recently observed screen. */
|
|
6416
|
+
currentScreenId = null;
|
|
6417
|
+
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
6418
|
+
iteration = 0;
|
|
6419
|
+
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
6420
|
+
lastProgressMark = 0;
|
|
6421
|
+
/** Consecutive iterations observed with no progress. */
|
|
6422
|
+
staleIterations = 0;
|
|
6423
|
+
staleThreshold;
|
|
6424
|
+
constructor(options = {}) {
|
|
6425
|
+
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
6426
|
+
}
|
|
6427
|
+
// -------------------------------------------------------------------------
|
|
6428
|
+
// Observation recording
|
|
6429
|
+
// -------------------------------------------------------------------------
|
|
6430
|
+
/**
|
|
6431
|
+
* Returns true when `signal` identifies a screen this run has never observed
|
|
6432
|
+
* before. Pure read — does not mutate state.
|
|
6433
|
+
*/
|
|
6434
|
+
isNewScreen(signal) {
|
|
6435
|
+
return !this.screens.has(deriveScreenId(signal));
|
|
6436
|
+
}
|
|
6437
|
+
/**
|
|
6438
|
+
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
6439
|
+
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
6440
|
+
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
6441
|
+
* element refresh). A driver observation already contains the structural UI
|
|
6442
|
+
* tree, elements, screenshot association, and screen identity, so it is
|
|
6443
|
+
* captured evidence immediately; the model's capture_screen call only adds
|
|
6444
|
+
* a friendly name/type. Returns the resolved screen-id.
|
|
6445
|
+
*/
|
|
6446
|
+
recordObservation(signal, screen, iteration) {
|
|
6447
|
+
this.iteration = iteration;
|
|
6448
|
+
const screenId = deriveScreenId(signal);
|
|
6449
|
+
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
6450
|
+
const existing = this.screens.get(screenId);
|
|
6451
|
+
if (existing) {
|
|
6452
|
+
this.screens.set(screenId, {
|
|
6453
|
+
...existing,
|
|
6454
|
+
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
6455
|
+
});
|
|
6456
|
+
} else {
|
|
6457
|
+
this.screens.set(screenId, {
|
|
6458
|
+
screenId,
|
|
6459
|
+
firstSeenIteration: iteration,
|
|
6460
|
+
elements: evidenceElements,
|
|
6461
|
+
visited: true
|
|
6462
|
+
});
|
|
6463
|
+
}
|
|
6464
|
+
this.frontier.delete(screenId);
|
|
6465
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6466
|
+
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
6467
|
+
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
6468
|
+
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
6469
|
+
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
6470
|
+
}
|
|
6471
|
+
for (const element of evidenceElements) {
|
|
6472
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6473
|
+
if (!candidate)
|
|
6474
|
+
continue;
|
|
6475
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6476
|
+
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
6477
|
+
controls.set(storedIdentity, {
|
|
6478
|
+
...candidate,
|
|
6479
|
+
identity: storedIdentity,
|
|
6480
|
+
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
6481
|
+
});
|
|
6482
|
+
}
|
|
6483
|
+
if (controls.size > 0)
|
|
6484
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6485
|
+
this.currentScreenId = screenId;
|
|
6486
|
+
this.updateStaleProgress();
|
|
6487
|
+
return screenId;
|
|
6488
|
+
}
|
|
6489
|
+
/**
|
|
6490
|
+
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
6491
|
+
* marks structural evidence visited; this call only resolves a rare pending
|
|
6492
|
+
* transition frontier and preserves callers that annotate explicitly.
|
|
6493
|
+
*/
|
|
6494
|
+
markVisited(screenId) {
|
|
6495
|
+
const state2 = this.screens.get(screenId);
|
|
6496
|
+
if (state2 && !state2.visited) {
|
|
6497
|
+
this.screens.set(screenId, { ...state2, visited: true });
|
|
6498
|
+
}
|
|
6499
|
+
this.frontier.delete(screenId);
|
|
6500
|
+
}
|
|
6501
|
+
/**
|
|
6502
|
+
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
6503
|
+
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
6504
|
+
* failed actions must remain on the breadth frontier.
|
|
6505
|
+
*/
|
|
6506
|
+
noteControlAttempt(screenId, element) {
|
|
6507
|
+
if (!screenId)
|
|
6508
|
+
return false;
|
|
6509
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6510
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6511
|
+
if (candidate) {
|
|
6512
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6513
|
+
const existing = controls.get(storedIdentity);
|
|
6514
|
+
controls.set(storedIdentity, {
|
|
6515
|
+
...candidate,
|
|
6516
|
+
identity: storedIdentity,
|
|
6517
|
+
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
6518
|
+
});
|
|
6519
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6520
|
+
}
|
|
6521
|
+
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6522
|
+
if (!tappedLabel)
|
|
6523
|
+
return false;
|
|
6524
|
+
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
6525
|
+
const alias = semanticControlAlias(tappedLabel);
|
|
6526
|
+
for (const control of controls.values()) {
|
|
6527
|
+
if (semanticControlAlias(control.label) === alias) {
|
|
6528
|
+
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
6529
|
+
if (control.sharedCoverageIdentity) {
|
|
6530
|
+
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
6535
|
+
this.successfulSafeControlAttemptCount += 1;
|
|
6536
|
+
this.staleIterations = 0;
|
|
6537
|
+
return true;
|
|
6538
|
+
}
|
|
6539
|
+
return false;
|
|
6540
|
+
}
|
|
6541
|
+
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
6542
|
+
annotateScreen(screenId, annotation) {
|
|
6543
|
+
const state2 = this.screens.get(screenId);
|
|
6544
|
+
if (!state2)
|
|
6545
|
+
return;
|
|
6546
|
+
this.screens.set(screenId, {
|
|
6547
|
+
...state2,
|
|
6548
|
+
screenName: annotation.screenName ?? state2.screenName,
|
|
6549
|
+
screenType: annotation.screenType ?? state2.screenType
|
|
6550
|
+
});
|
|
6551
|
+
}
|
|
6552
|
+
/** Attach structurally observed authentication provenance to a screen. */
|
|
6553
|
+
setScreenAuthEvidence(screenId, evidence) {
|
|
6554
|
+
const state2 = this.screens.get(screenId);
|
|
6555
|
+
if (!state2)
|
|
6556
|
+
return;
|
|
6557
|
+
this.screens.set(screenId, {
|
|
6558
|
+
...state2,
|
|
6559
|
+
requiresAuth: evidence.requiresAuth,
|
|
6560
|
+
authProvenance: evidence.authProvenance,
|
|
6561
|
+
sessionState: { ...evidence.sessionState }
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
/** Append a free-form observation note to a screen (observations tool). */
|
|
6565
|
+
addScreenObservation(screenId, note) {
|
|
6566
|
+
const state2 = this.screens.get(screenId);
|
|
6567
|
+
if (!state2)
|
|
6568
|
+
return;
|
|
6569
|
+
const normalized = note.trim();
|
|
6570
|
+
if (!normalized)
|
|
6571
|
+
return;
|
|
6572
|
+
const current = state2.observations ?? [];
|
|
6573
|
+
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
6574
|
+
return;
|
|
6575
|
+
}
|
|
6576
|
+
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
6577
|
+
this.screens.set(screenId, { ...state2, observations });
|
|
6578
|
+
}
|
|
6579
|
+
/**
|
|
6580
|
+
* Record a directed transition between two screens. Both endpoints are
|
|
6581
|
+
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
6582
|
+
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
6583
|
+
* `toScreenId` becomes the current screen when the transition starts from the
|
|
6584
|
+
* current screen.
|
|
6585
|
+
*/
|
|
6586
|
+
recordTransition(transition) {
|
|
6587
|
+
let recordedTransition = transition;
|
|
6588
|
+
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
6589
|
+
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
6590
|
+
if (sourceElement) {
|
|
6591
|
+
recordedTransition = {
|
|
6592
|
+
...transition,
|
|
6593
|
+
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
6598
|
+
if (!alreadyRecorded) {
|
|
6599
|
+
this.transitions.push(recordedTransition);
|
|
6600
|
+
}
|
|
6601
|
+
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
6602
|
+
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
6603
|
+
this.frontier.set(endpointId, {
|
|
6604
|
+
screenId: endpointId,
|
|
6605
|
+
source: "transition",
|
|
6606
|
+
discoveredAtIteration: this.iteration
|
|
6607
|
+
});
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
if (transition.fromScreenId === this.currentScreenId) {
|
|
6611
|
+
this.currentScreenId = transition.toScreenId;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
// -------------------------------------------------------------------------
|
|
6615
|
+
// Selectors
|
|
6616
|
+
// -------------------------------------------------------------------------
|
|
6617
|
+
/** The current screen-id (most recently observed), or null before any observation. */
|
|
6618
|
+
getCurrentScreenId() {
|
|
6619
|
+
return this.currentScreenId;
|
|
6620
|
+
}
|
|
6621
|
+
/** ScreenState for `screenId`, if known. */
|
|
6622
|
+
getScreen(screenId) {
|
|
6623
|
+
return this.screens.get(screenId);
|
|
6624
|
+
}
|
|
6625
|
+
/** Number of distinct screens observed this run. */
|
|
6626
|
+
getScreenCount() {
|
|
6627
|
+
return this.screens.size;
|
|
6628
|
+
}
|
|
6629
|
+
/** Number of screens backed by a live structural driver observation. */
|
|
6630
|
+
getVisitedScreenCount() {
|
|
6631
|
+
let total = 0;
|
|
6632
|
+
for (const state2 of this.screens.values()) {
|
|
6633
|
+
if (state2.visited)
|
|
6634
|
+
total += 1;
|
|
6635
|
+
}
|
|
6636
|
+
return total;
|
|
6637
|
+
}
|
|
6638
|
+
/** Total elements observed across all known screens (latest snapshot each). */
|
|
6639
|
+
getTotalElementCount() {
|
|
6640
|
+
let total = 0;
|
|
6641
|
+
for (const state2 of this.screens.values()) {
|
|
6642
|
+
total += state2.elements.length;
|
|
6643
|
+
}
|
|
6644
|
+
return total;
|
|
6645
|
+
}
|
|
6646
|
+
/**
|
|
6647
|
+
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
6648
|
+
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
6649
|
+
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
6650
|
+
* which has no mobile analog.
|
|
6651
|
+
*/
|
|
6652
|
+
getFrontier() {
|
|
6653
|
+
return [...this.frontier.values()].filter((item) => {
|
|
6654
|
+
const state2 = this.screens.get(item.screenId);
|
|
6655
|
+
return !state2 || !state2.visited;
|
|
6656
|
+
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
6657
|
+
}
|
|
6658
|
+
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
6659
|
+
getUnexploredSafeNavigationCandidates() {
|
|
6660
|
+
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
6661
|
+
const candidates = [];
|
|
6662
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6663
|
+
const origin = this.screens.get(screenId);
|
|
6664
|
+
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
6665
|
+
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
6666
|
+
continue;
|
|
6667
|
+
for (const control of controls.values()) {
|
|
6668
|
+
const alias = semanticControlAlias(control.label);
|
|
6669
|
+
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
6670
|
+
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
6671
|
+
continue;
|
|
6672
|
+
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
6673
|
+
if (existing) {
|
|
6674
|
+
existing.screenIds.add(screenId);
|
|
6675
|
+
} else {
|
|
6676
|
+
candidates.push({
|
|
6677
|
+
identity: coverageIdentity,
|
|
6678
|
+
label: control.label,
|
|
6679
|
+
kind: control.kind,
|
|
6680
|
+
alias,
|
|
6681
|
+
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
return candidates.map((candidate) => ({
|
|
6687
|
+
identity: candidate.identity,
|
|
6688
|
+
label: candidate.label,
|
|
6689
|
+
kind: candidate.kind,
|
|
6690
|
+
screenIds: [...candidate.screenIds]
|
|
6691
|
+
})).sort((a, b) => {
|
|
6692
|
+
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
6693
|
+
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
6694
|
+
if (aCurrent !== bCurrent)
|
|
6695
|
+
return aCurrent ? -1 : 1;
|
|
6696
|
+
return b.screenIds.length - a.screenIds.length;
|
|
6697
|
+
});
|
|
6698
|
+
}
|
|
6699
|
+
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
6700
|
+
getSurveyBreadthCoverage() {
|
|
6701
|
+
const unvisitedScreenIds = this.getFrontier();
|
|
6702
|
+
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
6703
|
+
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
6704
|
+
const missing = [];
|
|
6705
|
+
if (unvisitedScreenIds.length > 0) {
|
|
6706
|
+
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
6707
|
+
}
|
|
6708
|
+
if (unexploredControls.length > 0) {
|
|
6709
|
+
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
6710
|
+
}
|
|
6711
|
+
return {
|
|
6712
|
+
required: true,
|
|
6713
|
+
complete,
|
|
6714
|
+
unvisitedScreenIds,
|
|
6715
|
+
unexploredControls,
|
|
6716
|
+
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
6717
|
+
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
6718
|
+
};
|
|
6719
|
+
}
|
|
6720
|
+
/**
|
|
6721
|
+
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
6722
|
+
* cardinality alone is insufficient: attempting control A can reveal control
|
|
6723
|
+
* B and leave the remaining count unchanged even though real work happened.
|
|
6724
|
+
*/
|
|
6725
|
+
getSurveyBreadthProgressMark() {
|
|
6726
|
+
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
6727
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6728
|
+
for (const control of controls.values()) {
|
|
6729
|
+
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
6732
|
+
return [
|
|
6733
|
+
`screens:${this.screens.size}`,
|
|
6734
|
+
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
6735
|
+
`controls:${discoveredControlIdentities.size}`,
|
|
6736
|
+
`transitions:${this.transitions.length}`
|
|
6737
|
+
].join("|");
|
|
6738
|
+
}
|
|
6739
|
+
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
6740
|
+
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
6741
|
+
let best;
|
|
6742
|
+
let bestIntersection = 0;
|
|
6743
|
+
for (const family of families) {
|
|
6744
|
+
let intersection = 0;
|
|
6745
|
+
for (const label of labels) {
|
|
6746
|
+
if (family.labels.has(label))
|
|
6747
|
+
intersection += 1;
|
|
6748
|
+
}
|
|
6749
|
+
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
6750
|
+
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
6751
|
+
best = family;
|
|
6752
|
+
bestIntersection = intersection;
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
if (best) {
|
|
6756
|
+
for (const label of labels)
|
|
6757
|
+
best.labels.add(label);
|
|
6758
|
+
return best.id;
|
|
6759
|
+
}
|
|
6760
|
+
const created = {
|
|
6761
|
+
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
6762
|
+
labels: new Set(labels)
|
|
6763
|
+
};
|
|
6764
|
+
families.push(created);
|
|
6765
|
+
this.semanticNavigationFamilies.set(groupKey, families);
|
|
6766
|
+
return created.id;
|
|
6767
|
+
}
|
|
6768
|
+
/** Observed transitions (defensive copy). */
|
|
6769
|
+
getTransitions() {
|
|
6770
|
+
return [...this.transitions];
|
|
6771
|
+
}
|
|
6772
|
+
// -------------------------------------------------------------------------
|
|
6773
|
+
// Stale-progress detection (ported semantics from the web stale guard)
|
|
6774
|
+
// -------------------------------------------------------------------------
|
|
6775
|
+
/**
|
|
6776
|
+
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
6777
|
+
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
6778
|
+
* the stale detector triggers when the mark stops advancing for
|
|
6779
|
+
* `staleThreshold` consecutive iterations.
|
|
6780
|
+
*/
|
|
6781
|
+
getProgressMark() {
|
|
6782
|
+
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
6783
|
+
}
|
|
6784
|
+
updateStaleProgress() {
|
|
6785
|
+
const mark = this.getProgressMark();
|
|
6786
|
+
if (mark > this.lastProgressMark) {
|
|
6787
|
+
this.lastProgressMark = mark;
|
|
6788
|
+
this.staleIterations = 0;
|
|
6789
|
+
} else {
|
|
6790
|
+
this.staleIterations += 1;
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
/** Consecutive no-progress iterations observed so far. */
|
|
6794
|
+
getStaleIterations() {
|
|
6795
|
+
return this.staleIterations;
|
|
6796
|
+
}
|
|
6797
|
+
/**
|
|
6798
|
+
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
6799
|
+
* new screen and no new element — the signal the explorer uses to nudge or
|
|
6800
|
+
* finish (mirrors the web explorer's stale guard).
|
|
6801
|
+
*/
|
|
6802
|
+
isStaleProgress() {
|
|
6803
|
+
return this.staleIterations >= this.staleThreshold;
|
|
6804
|
+
}
|
|
6805
|
+
// -------------------------------------------------------------------------
|
|
6806
|
+
// Planner evidence export
|
|
6807
|
+
// -------------------------------------------------------------------------
|
|
6808
|
+
/**
|
|
6809
|
+
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
6810
|
+
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
6811
|
+
* state.
|
|
6812
|
+
*/
|
|
6813
|
+
toScreenGraph() {
|
|
6814
|
+
const screens = /* @__PURE__ */ new Map();
|
|
6815
|
+
for (const [id, state2] of this.screens) {
|
|
6816
|
+
screens.set(id, {
|
|
6817
|
+
...state2,
|
|
6818
|
+
elements: [...state2.elements],
|
|
6819
|
+
observations: state2.observations ? [...state2.observations] : void 0,
|
|
6820
|
+
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
6821
|
+
});
|
|
6822
|
+
}
|
|
6823
|
+
return {
|
|
6824
|
+
screens,
|
|
6825
|
+
transitions: this.transitions.map((t) => ({
|
|
6826
|
+
fromScreenId: t.fromScreenId,
|
|
6827
|
+
toScreenId: t.toScreenId,
|
|
6828
|
+
viaRef: t.viaRef,
|
|
6829
|
+
viaControlIdentity: t.viaControlIdentity,
|
|
6830
|
+
action: t.action
|
|
6831
|
+
}))
|
|
6832
|
+
};
|
|
6833
|
+
}
|
|
6834
|
+
/**
|
|
6835
|
+
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
6836
|
+
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
6837
|
+
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
6838
|
+
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
6839
|
+
* the screen's parsed actionable elements into the platform-neutral
|
|
6840
|
+
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
6841
|
+
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
6842
|
+
* intelligence). `triggersNavigation` is derived from the durable source
|
|
6843
|
+
* element identity captured when the transition happened (with ref-only
|
|
6844
|
+
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
6845
|
+
*/
|
|
6846
|
+
buildInMemoryScreenMap() {
|
|
6847
|
+
const pages = [];
|
|
6848
|
+
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
6849
|
+
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
6850
|
+
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
6851
|
+
for (const transition of this.transitions) {
|
|
6852
|
+
if (transition.viaControlIdentity) {
|
|
6853
|
+
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6854
|
+
set.add(transition.viaControlIdentity);
|
|
6855
|
+
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
6856
|
+
} else if (transition.viaRef) {
|
|
6857
|
+
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6858
|
+
set.add(transition.viaRef);
|
|
6859
|
+
navRefsByScreen.set(transition.fromScreenId, set);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
for (const state2 of ordered) {
|
|
6863
|
+
const page = {
|
|
6864
|
+
route: state2.screenId,
|
|
6865
|
+
screenId: state2.screenId,
|
|
6866
|
+
pageType: state2.screenType ?? "UNKNOWN",
|
|
6867
|
+
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
6868
|
+
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
6869
|
+
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
6870
|
+
}),
|
|
6871
|
+
requiresAuth: state2.requiresAuth ?? false
|
|
6872
|
+
};
|
|
6873
|
+
if (state2.sessionState) {
|
|
6874
|
+
page.sessionState = { ...state2.sessionState };
|
|
6875
|
+
}
|
|
6876
|
+
if (state2.screenName) {
|
|
6877
|
+
page.title = state2.screenName;
|
|
6878
|
+
}
|
|
6879
|
+
if (state2.observations && state2.observations.length > 0) {
|
|
6880
|
+
page.observations = state2.observations.map((note) => ({
|
|
6881
|
+
action: "observe",
|
|
6882
|
+
outcome: note,
|
|
6883
|
+
outcomeType: "SUCCESS"
|
|
6884
|
+
}));
|
|
6885
|
+
}
|
|
6886
|
+
pages.push(page);
|
|
6887
|
+
}
|
|
6888
|
+
return pages;
|
|
6889
|
+
}
|
|
6890
|
+
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
6891
|
+
toScreenMap() {
|
|
6892
|
+
return this.buildInMemoryScreenMap();
|
|
6893
|
+
}
|
|
6894
|
+
};
|
|
6895
|
+
exports2.MobileExplorationState = MobileExplorationState;
|
|
6896
|
+
}
|
|
6897
|
+
});
|
|
6898
|
+
|
|
6216
6899
|
// ../runner-core/dist/services/mobile/mobile-keyboard.js
|
|
6217
6900
|
var require_mobile_keyboard = __commonJS({
|
|
6218
6901
|
"../runner-core/dist/services/mobile/mobile-keyboard.js"(exports2) {
|
|
@@ -63931,6 +64614,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
63931
64614
|
var ai_retry_js_1 = require_ai_retry();
|
|
63932
64615
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
63933
64616
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64617
|
+
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
63934
64618
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
63935
64619
|
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
63936
64620
|
var mobile_auth_coverage_js_1 = require_mobile_auth_coverage();
|
|
@@ -64288,6 +64972,10 @@ var require_mobile_explorer = __commonJS({
|
|
|
64288
64972
|
return void 0;
|
|
64289
64973
|
return normalized;
|
|
64290
64974
|
}
|
|
64975
|
+
function destructiveActionRefusal(label, kind = "tap") {
|
|
64976
|
+
const what = kind === "confirmation" ? `Typing "${label}" is a destructive confirmation` : `Tapping "${label}" is an irreversible, high-stakes destructive action`;
|
|
64977
|
+
return `Refused: ${what} (account/data deletion, deactivation, wipe, or similar). Autonomous testing must never execute irreversible destruction on the account under test. Record it as an untested destructive flow in your summary and choose a non-destructive control instead.`;
|
|
64978
|
+
}
|
|
64291
64979
|
function hybridElementForEvidence(element, credentials) {
|
|
64292
64980
|
const id = safeHybridIdentity(element.id, credentials);
|
|
64293
64981
|
const name = safeHybridIdentity(element.name, credentials);
|
|
@@ -64910,7 +65598,7 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
64910
65598
|
3. After each navigation, capture_screen the destination and record_transition. After completing a meaningful flow (login, search, create), ${enableRecordJourney ? "call record_journey with the steps and expected outcome." : "continue exploring."}
|
|
64911
65599
|
4. Use mobile_relaunch for a clean, hermetic restart before testing a distinct flow that needs fresh state.
|
|
64912
65600
|
5. mobile_swipe to scroll; mobile_back to return.
|
|
64913
|
-
6. Do NOT trigger irreversible side effects
|
|
65601
|
+
6. Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages/invites to others; and ESPECIALLY account/data deletion, deactivation, "delete account", factory reset, or any "type DELETE to confirm" gate. The runner hard-blocks destructive taps and confirmation keystrokes, so attempting them only wastes an iteration. Note such flows in your summary and explore elsewhere.
|
|
64914
65602
|
7. Stay inside this app.${targetLine} If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back or mobile_relaunch.
|
|
64915
65603
|
|
|
64916
65604
|
Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
@@ -65775,6 +66463,11 @@ Exploration complete: ${summary}`);
|
|
|
65775
66463
|
if (!driver.webTap) {
|
|
65776
66464
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW taps." };
|
|
65777
66465
|
}
|
|
66466
|
+
const hybridTapLabel = hybridElementForEvidence(hybrid, redactionCredentials).label ?? "";
|
|
66467
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(hybridTapLabel)) {
|
|
66468
|
+
log2(` \u26A0 Refused destructive tap on "${hybridTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66469
|
+
return { ok: false, error: destructiveActionRefusal(hybridTapLabel) };
|
|
66470
|
+
}
|
|
65778
66471
|
const authSurfaceBeforeTap2 = currentAuthSurface();
|
|
65779
66472
|
const fromScreenId2 = state2.getCurrentScreenId();
|
|
65780
66473
|
const actionResult2 = await driver.webTap(hybrid.webviewContext, hybrid.css);
|
|
@@ -65805,6 +66498,11 @@ Exploration complete: ${summary}`);
|
|
|
65805
66498
|
if (!resolved) {
|
|
65806
66499
|
return { ok: false, error: `Unknown element ref "${String(toolArgs.ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.` };
|
|
65807
66500
|
}
|
|
66501
|
+
const nativeTapLabel = resolved.element.label ?? resolved.element.text ?? resolved.element.accessibilityId ?? "";
|
|
66502
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(nativeTapLabel)) {
|
|
66503
|
+
log2(` \u26A0 Refused destructive tap on "${nativeTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66504
|
+
return { ok: false, error: destructiveActionRefusal(nativeTapLabel) };
|
|
66505
|
+
}
|
|
65808
66506
|
const fromScreenId = state2.getCurrentScreenId();
|
|
65809
66507
|
const boundaryBeforeAction = currentBoundary();
|
|
65810
66508
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
@@ -65845,6 +66543,10 @@ Exploration complete: ${summary}`);
|
|
|
65845
66543
|
// ----- Type -----
|
|
65846
66544
|
case "mobile_type": {
|
|
65847
66545
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
66546
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveConfirmationInput)(value)) {
|
|
66547
|
+
log2(` \u26A0 Refused destructive confirmation input "${value}"; noting instead.`);
|
|
66548
|
+
return { ok: false, error: destructiveActionRefusal(value, "confirmation") };
|
|
66549
|
+
}
|
|
65848
66550
|
const allowedLiteral = inbox ? inbox.state.email : "";
|
|
65849
66551
|
for (const [key, secret] of Object.entries(credentials)) {
|
|
65850
66552
|
if (!secret || secret.length < 4)
|
|
@@ -70554,7 +71256,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70554
71256
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
70555
71257
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
70556
71258
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
70557
|
-
- Do NOT trigger irreversible side effects
|
|
71259
|
+
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
70558
71260
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
70559
71261
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
70560
71262
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -79227,6 +79929,34 @@ var require_anchor_harvester = __commonJS({
|
|
|
79227
79929
|
}
|
|
79228
79930
|
});
|
|
79229
79931
|
|
|
79932
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
79933
|
+
var require_discovery_lint_flags = __commonJS({
|
|
79934
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
79935
|
+
"use strict";
|
|
79936
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
79937
|
+
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
79938
|
+
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
79939
|
+
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
79940
|
+
function strictOn(value) {
|
|
79941
|
+
return value === "true" || value === "1";
|
|
79942
|
+
}
|
|
79943
|
+
function structuralLocatorLintMode() {
|
|
79944
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
79945
|
+
if (v === "warn")
|
|
79946
|
+
return "warn";
|
|
79947
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
79948
|
+
return "enforce";
|
|
79949
|
+
return "off";
|
|
79950
|
+
}
|
|
79951
|
+
function isPwMethodPreflightEnabled() {
|
|
79952
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
79953
|
+
}
|
|
79954
|
+
function isVqaLoginDedupeEnabled() {
|
|
79955
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
79956
|
+
}
|
|
79957
|
+
}
|
|
79958
|
+
});
|
|
79959
|
+
|
|
79230
79960
|
// ../runner-core/dist/services/auth-recipe.js
|
|
79231
79961
|
var require_auth_recipe = __commonJS({
|
|
79232
79962
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
@@ -79249,9 +79979,11 @@ var require_auth_recipe = __commonJS({
|
|
|
79249
79979
|
exports2.extractAuthRegion = extractAuthRegion;
|
|
79250
79980
|
exports2.stripAuthRegions = stripAuthRegions;
|
|
79251
79981
|
exports2.ensureExpectImported = ensureExpectImported;
|
|
79982
|
+
exports2.ensureLoginCall = ensureLoginCall;
|
|
79252
79983
|
exports2.upsertAuthBlock = upsertAuthBlock;
|
|
79253
79984
|
exports2.buildHealAuthStamp = buildHealAuthStamp;
|
|
79254
79985
|
var crypto_1 = require("crypto");
|
|
79986
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
79255
79987
|
exports2.AUTH_MARKER_VERSION = "v1";
|
|
79256
79988
|
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
79257
79989
|
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
|
|
@@ -79515,10 +80247,13 @@ ${tail}`;
|
|
|
79515
80247
|
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
79516
80248
|
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
79517
80249
|
function ensureLoginCall(code) {
|
|
79518
|
-
|
|
79519
|
-
if (matches.length === 0)
|
|
80250
|
+
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
79520
80251
|
return code;
|
|
79521
80252
|
let result = code;
|
|
80253
|
+
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
80254
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
80255
|
+
}
|
|
80256
|
+
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
79522
80257
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
79523
80258
|
const m = matches[i];
|
|
79524
80259
|
if (m.index === void 0)
|
|
@@ -83410,7 +84145,7 @@ var require_discover_generator = __commonJS({
|
|
|
83410
84145
|
"../runner-core/dist/services/discover-generator.js"(exports2) {
|
|
83411
84146
|
"use strict";
|
|
83412
84147
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
83413
|
-
exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
84148
|
+
exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.VALID_PLAYWRIGHT_GETBY_METHODS = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
83414
84149
|
exports2.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
|
|
83415
84150
|
exports2.detectScenarioSignals = detectScenarioSignals;
|
|
83416
84151
|
exports2.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
|
|
@@ -83433,6 +84168,7 @@ var require_discover_generator = __commonJS({
|
|
|
83433
84168
|
exports2.upgradePositionalProvenLines = upgradePositionalProvenLines;
|
|
83434
84169
|
exports2.synthesizeRefActionLines = synthesizeRefActionLines;
|
|
83435
84170
|
exports2.renderSessionRecording = renderSessionRecording;
|
|
84171
|
+
exports2.scanFragileStructuralLocators = scanFragileStructuralLocators;
|
|
83436
84172
|
exports2.validateFinalActionFidelity = validateFinalActionFidelity;
|
|
83437
84173
|
exports2.resolveSuspectedBugVerdict = resolveSuspectedBugVerdict;
|
|
83438
84174
|
exports2.buildScenarioSystemPrompt = buildScenarioSystemPrompt;
|
|
@@ -83460,6 +84196,7 @@ var require_discover_generator = __commonJS({
|
|
|
83460
84196
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
83461
84197
|
var auth_replay_js_1 = require_auth_replay();
|
|
83462
84198
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
84199
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
83463
84200
|
exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
83464
84201
|
"browser_install",
|
|
83465
84202
|
"browser_close",
|
|
@@ -84184,6 +84921,15 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
84184
84921
|
return assembled.join(separator);
|
|
84185
84922
|
}
|
|
84186
84923
|
var SENSITIVE_ENV_KEY_RE = /email|password|token|secret|api[_-]?key|access[_-]?key|username|user(name)?|bearer|session|cookie|\bpat\b|refresh|jwt/i;
|
|
84924
|
+
exports2.VALID_PLAYWRIGHT_GETBY_METHODS = /* @__PURE__ */ new Set([
|
|
84925
|
+
"getByRole",
|
|
84926
|
+
"getByText",
|
|
84927
|
+
"getByLabel",
|
|
84928
|
+
"getByPlaceholder",
|
|
84929
|
+
"getByTestId",
|
|
84930
|
+
"getByAltText",
|
|
84931
|
+
"getByTitle"
|
|
84932
|
+
]);
|
|
84187
84933
|
var VALID_PLAYWRIGHT_MATCHERS = /* @__PURE__ */ new Set([
|
|
84188
84934
|
// Locator assertions
|
|
84189
84935
|
"toBeAttached",
|
|
@@ -84671,6 +85417,16 @@ ${testOpen}`;
|
|
|
84671
85417
|
return false;
|
|
84672
85418
|
}
|
|
84673
85419
|
}
|
|
85420
|
+
if ((0, discovery_lint_flags_js_1.isPwMethodPreflightEnabled)()) {
|
|
85421
|
+
const getByCalls = codeForMatcherScan.match(/\.getBy[A-Za-z0-9_]*\s*\(/g) ?? [];
|
|
85422
|
+
for (const call of getByCalls) {
|
|
85423
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
85424
|
+
if (!exports2.VALID_PLAYWRIGHT_GETBY_METHODS.has(methodName)) {
|
|
85425
|
+
logs2?.push(` \u2717 Rejected "${t.name}": invalid Playwright locator method "${methodName}" (not a real getBy* API \u2014 likely hallucinated)`);
|
|
85426
|
+
return false;
|
|
85427
|
+
}
|
|
85428
|
+
}
|
|
85429
|
+
}
|
|
84674
85430
|
if (code.includes("// TODO")) {
|
|
84675
85431
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
84676
85432
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -85784,6 +86540,35 @@ ${testOpen}`;
|
|
|
85784
86540
|
}
|
|
85785
86541
|
return null;
|
|
85786
86542
|
}
|
|
86543
|
+
var STRUCTURAL_ACTION_RE = /\.(?:click|dblclick|tap|fill|type|pressSequentially|press|selectOption|hover|check|uncheck|setChecked|setInputFiles|dragTo|focus|clear)\s*\(/;
|
|
86544
|
+
var POSITIONAL_PSEUDO_RE = /:(?:last-child|first-child|nth-child|nth-of-type|nth-last-child|nth-last-of-type)\b/i;
|
|
86545
|
+
var TABLE_TOKEN_RE = /\b(?:tr|td|th|tbody|thead)\b/i;
|
|
86546
|
+
var ROW_ANCHOR_RE = /getByRole\s*\(\s*['"`]row['"`]|getByTestId|data-testid|data-test\b|\[role\s*=\s*['"`]row['"`]\]/i;
|
|
86547
|
+
var LOCATOR_ARG_RE = /\.locator\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
|
|
86548
|
+
function scanFragileStructuralLocators(code) {
|
|
86549
|
+
const findings = [];
|
|
86550
|
+
const scanCode = code.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
86551
|
+
for (const rawLine of scanCode.split("\n")) {
|
|
86552
|
+
const line = rawLine.replace(/\/\/.*$/, "");
|
|
86553
|
+
if (!STRUCTURAL_ACTION_RE.test(line))
|
|
86554
|
+
continue;
|
|
86555
|
+
if (ROW_ANCHOR_RE.test(line))
|
|
86556
|
+
continue;
|
|
86557
|
+
LOCATOR_ARG_RE.lastIndex = 0;
|
|
86558
|
+
let m;
|
|
86559
|
+
while ((m = LOCATOR_ARG_RE.exec(line)) !== null) {
|
|
86560
|
+
const css = m[2];
|
|
86561
|
+
if (POSITIONAL_PSEUDO_RE.test(css) && TABLE_TOKEN_RE.test(css)) {
|
|
86562
|
+
findings.push({
|
|
86563
|
+
line: rawLine.trim().slice(0, 200),
|
|
86564
|
+
reason: `Positional table-row selector "${css.slice(0, 120)}" is not scoped to a row identity \u2014 it breaks when rows reorder, sort, filter, or a row is inserted. Scope through the row: page.getByRole('row', { name: '<a stable cell value in that row>' }).getByRole('button', { name: '<verb>' }).`
|
|
86565
|
+
});
|
|
86566
|
+
break;
|
|
86567
|
+
}
|
|
86568
|
+
}
|
|
86569
|
+
}
|
|
86570
|
+
return findings;
|
|
86571
|
+
}
|
|
85787
86572
|
function validateFinalActionFidelity(playwrightCode, actionLedger, credentials, opts) {
|
|
85788
86573
|
const ledgerSigs = [];
|
|
85789
86574
|
for (const entry of actionLedger) {
|
|
@@ -86223,6 +87008,7 @@ ${tailBlock}`;
|
|
|
86223
87008
|
let actionRevisionAttempted = false;
|
|
86224
87009
|
let ambiguityRevisionAttempted = false;
|
|
86225
87010
|
let strengthRevisionAttempted = false;
|
|
87011
|
+
let structuralRevisionAttempted = false;
|
|
86226
87012
|
let lastRenderedLedgerEntryCount = 0;
|
|
86227
87013
|
const successfulVerifications = [];
|
|
86228
87014
|
const actionLedger = [];
|
|
@@ -86493,6 +87279,33 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
86493
87279
|
});
|
|
86494
87280
|
continue;
|
|
86495
87281
|
}
|
|
87282
|
+
const structuralLintMode = (0, discovery_lint_flags_js_1.structuralLocatorLintMode)();
|
|
87283
|
+
const structuralFindings = structuralLintMode !== "off" ? scanFragileStructuralLocators(validated[0].playwrightCode) : [];
|
|
87284
|
+
if (structuralLintMode === "warn" && structuralFindings.length > 0) {
|
|
87285
|
+
logs2.push(` Structural-locator gate [warn]: ${structuralFindings.length} positional table-row selector(s) \u2014 ${structuralFindings.map((f) => sanitizeLedgerText(f.line)).join(" | ").slice(0, 300)}`);
|
|
87286
|
+
}
|
|
87287
|
+
if (structuralLintMode === "enforce" && structuralFindings.length > 0 && !structuralRevisionAttempted) {
|
|
87288
|
+
structuralRevisionAttempted = true;
|
|
87289
|
+
const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
|
|
87290
|
+
if (renderedLedger)
|
|
87291
|
+
lastRenderedLedgerEntryCount = actionLedger.length;
|
|
87292
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) found \u2014 requesting one revision (recording attached: ${renderedLedger ? "yes" : "no"})`);
|
|
87293
|
+
const structuralPayload = {
|
|
87294
|
+
acknowledged: false,
|
|
87295
|
+
valid: false,
|
|
87296
|
+
revisionRequired: true,
|
|
87297
|
+
reason: "fragile_structural_locator",
|
|
87298
|
+
instruction: "One or more locators below select a table row by POSITION (tr:last-child, tr:nth-child, td:nth-child) instead of by identity. Positional row selectors break the moment rows are reordered, sorted, filtered, or a row is inserted.\nFor EACH flagged locator:\n1. Scope through the row IDENTITY: page.getByRole('row', { name: '<a stable cell value in that row \u2014 an id, name, email>' }).getByRole('button', { name: '<verb>' }).\n2. If the row genuinely has no stable text, use a data-testid on the row if one exists.\n3. Keep every other line exactly as-is. Submit the COMPLETE corrected test and call finish_generate again.",
|
|
87299
|
+
offendingLocators: structuralFindings.map((f) => ({
|
|
87300
|
+
line: sanitizeLedgerText(f.line),
|
|
87301
|
+
reason: f.reason
|
|
87302
|
+
}))
|
|
87303
|
+
};
|
|
87304
|
+
if (renderedLedger)
|
|
87305
|
+
structuralPayload.sessionRecording = renderedLedger;
|
|
87306
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(structuralPayload) });
|
|
87307
|
+
continue;
|
|
87308
|
+
}
|
|
86496
87309
|
const ambiguityGateEval = evaluateAmbiguityGate(validated[0].playwrightCode, ambiguityRevisionAttempted, iteration < MAX_SCENARIO_ITERATIONS);
|
|
86497
87310
|
const ambiguousLocators = ambiguityGateEval.findings;
|
|
86498
87311
|
if (ambiguityGateEval.fire) {
|
|
@@ -86589,12 +87402,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86589
87402
|
const strengthSeverity = strengthVerdict?.severity ?? "none";
|
|
86590
87403
|
const hasResidualViolations = qualityCheck.offendingLines.length > 0;
|
|
86591
87404
|
const hasResidualActionDrift = actionCheck.offendingActions.length > 0;
|
|
87405
|
+
const hasResidualStructural = structuralLintMode === "enforce" && structuralFindings.length > 0;
|
|
86592
87406
|
if (hasResidualViolations) {
|
|
86593
87407
|
logs2.push(` Assertion quality gate: violations remain after revision \u2014 downgrading to @needs-review`);
|
|
86594
87408
|
}
|
|
86595
87409
|
if (hasResidualActionDrift) {
|
|
86596
87410
|
logs2.push(` Action fidelity gate: ${actionCheck.offendingActions.length} action drift(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
86597
87411
|
}
|
|
87412
|
+
if (hasResidualStructural) {
|
|
87413
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
87414
|
+
}
|
|
86598
87415
|
let actualVerified = verified;
|
|
86599
87416
|
if (verified && verifyCallCount === 0) {
|
|
86600
87417
|
actualVerified = false;
|
|
@@ -86647,6 +87464,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86647
87464
|
residual: ambiguousLocators.length,
|
|
86648
87465
|
lines: ambiguousLocators.map((a) => sanitizeLedgerText(a.line))
|
|
86649
87466
|
},
|
|
87467
|
+
// G2 — omitted entirely when off so the off-path healingContext is
|
|
87468
|
+
// byte-identical; present in warn (measure) + enforce (act) modes.
|
|
87469
|
+
...structuralLintMode !== "off" ? {
|
|
87470
|
+
structuralGate: {
|
|
87471
|
+
mode: structuralLintMode,
|
|
87472
|
+
fired: structuralRevisionAttempted,
|
|
87473
|
+
residual: structuralFindings.length,
|
|
87474
|
+
lines: structuralFindings.map((f) => sanitizeLedgerText(f.line))
|
|
87475
|
+
}
|
|
87476
|
+
} : {},
|
|
86650
87477
|
// Durable marker the server promote-seam (failure-analyzer.checkTestStability)
|
|
86651
87478
|
// reads to decide ACTIVE vs QUARANTINED(@bug-found) vs hold-DRAFT. Omitted when
|
|
86652
87479
|
// the gate errored (fail-open) so a missing marker is treated as "promote".
|
|
@@ -86672,7 +87499,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86672
87499
|
verified: actualVerified,
|
|
86673
87500
|
healingContext: { ...baseHealingContext, gateActivity }
|
|
86674
87501
|
};
|
|
86675
|
-
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes) && !result.tags.includes("@needs-review")) {
|
|
87502
|
+
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes || hasResidualStructural) && !result.tags.includes("@needs-review")) {
|
|
86676
87503
|
result.tags.push("@needs-review");
|
|
86677
87504
|
}
|
|
86678
87505
|
if (actualVerified) {
|
|
@@ -320906,675 +321733,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
320906
321733
|
}
|
|
320907
321734
|
});
|
|
320908
321735
|
|
|
320909
|
-
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
320910
|
-
var require_mobile_exploration_state = __commonJS({
|
|
320911
|
-
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
|
|
320912
|
-
"use strict";
|
|
320913
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
320914
|
-
exports2.MobileExplorationState = void 0;
|
|
320915
|
-
exports2.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
320916
|
-
exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
320917
|
-
exports2.deriveScreenId = deriveScreenId;
|
|
320918
|
-
var mobile_observation_js_1 = require_mobile_observation();
|
|
320919
|
-
var DEFAULT_STALE_THRESHOLD = 4;
|
|
320920
|
-
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
320921
|
-
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
320922
|
-
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
320923
|
-
var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
|
|
320924
|
-
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
320925
|
-
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
320926
|
-
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
320927
|
-
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
320928
|
-
var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
|
|
320929
|
-
function normalizeControlIdentityPart(value) {
|
|
320930
|
-
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
320931
|
-
}
|
|
320932
|
-
function semanticControlAlias(label) {
|
|
320933
|
-
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
320934
|
-
}
|
|
320935
|
-
function localControlCoverageIdentity(screenId, identity) {
|
|
320936
|
-
return `screen:${screenId}:${identity}`;
|
|
320937
|
-
}
|
|
320938
|
-
function storedMobileControlIdentity(candidate) {
|
|
320939
|
-
return [
|
|
320940
|
-
candidate.identity,
|
|
320941
|
-
`kind:${candidate.kind}`,
|
|
320942
|
-
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
320943
|
-
].join(":");
|
|
320944
|
-
}
|
|
320945
|
-
function isSystemBackNavigationControl(element, label) {
|
|
320946
|
-
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
320947
|
-
return true;
|
|
320948
|
-
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
320949
|
-
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
320950
|
-
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
320951
|
-
});
|
|
320952
|
-
}
|
|
320953
|
-
function stableMobileControlIdentity(element) {
|
|
320954
|
-
const role = normalizeControlIdentityPart(element.role || "control");
|
|
320955
|
-
const accessibilityId = element.accessibilityId?.trim();
|
|
320956
|
-
if (accessibilityId)
|
|
320957
|
-
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
320958
|
-
const resourceId = element.resourceId?.trim();
|
|
320959
|
-
if (resourceId)
|
|
320960
|
-
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
320961
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320962
|
-
if (label)
|
|
320963
|
-
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
320964
|
-
return null;
|
|
320965
|
-
}
|
|
320966
|
-
function classifySafeMobileNavigationControl(element) {
|
|
320967
|
-
if (!element.actionable || element.enabled === false)
|
|
320968
|
-
return null;
|
|
320969
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320970
|
-
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
320971
|
-
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
320972
|
-
return null;
|
|
320973
|
-
if (isSystemBackNavigationControl(element, label))
|
|
320974
|
-
return null;
|
|
320975
|
-
const role = element.role.trim();
|
|
320976
|
-
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
320977
|
-
return null;
|
|
320978
|
-
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
320979
|
-
let kind = null;
|
|
320980
|
-
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
320981
|
-
kind = "tab";
|
|
320982
|
-
else if (/menuitem/i.test(role))
|
|
320983
|
-
kind = "menu-item";
|
|
320984
|
-
else if (linkLike && element.navigationProvenance)
|
|
320985
|
-
kind = "link";
|
|
320986
|
-
else if (linkLike)
|
|
320987
|
-
return null;
|
|
320988
|
-
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
320989
|
-
kind = "menu-trigger";
|
|
320990
|
-
else if (element.expanded === false)
|
|
320991
|
-
kind = "disclosure";
|
|
320992
|
-
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
320993
|
-
kind = "menu-trigger";
|
|
320994
|
-
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
320995
|
-
kind = "modal-trigger";
|
|
320996
|
-
else if (element.navigationProvenance)
|
|
320997
|
-
kind = "navigation-button";
|
|
320998
|
-
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
320999
|
-
kind = "navigation-button";
|
|
321000
|
-
if (!kind)
|
|
321001
|
-
return null;
|
|
321002
|
-
const identity = stableMobileControlIdentity(element);
|
|
321003
|
-
return identity ? { identity, label, kind } : null;
|
|
321004
|
-
}
|
|
321005
|
-
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
321006
|
-
if (!element.navigationProvenance)
|
|
321007
|
-
return void 0;
|
|
321008
|
-
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
321009
|
-
if (!durableId && !semanticFamilyId)
|
|
321010
|
-
return void 0;
|
|
321011
|
-
return [
|
|
321012
|
-
"shared-navigation",
|
|
321013
|
-
element.navigationProvenance,
|
|
321014
|
-
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
321015
|
-
candidate.kind,
|
|
321016
|
-
durableId || "semantic-only",
|
|
321017
|
-
normalizeControlIdentityPart(candidate.label)
|
|
321018
|
-
].filter(Boolean).join(":");
|
|
321019
|
-
}
|
|
321020
|
-
function mobileNavigationRegion(element, windowSize) {
|
|
321021
|
-
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
321022
|
-
return "unknown";
|
|
321023
|
-
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
321024
|
-
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
321025
|
-
if (centerY >= 0.72)
|
|
321026
|
-
return "bottom";
|
|
321027
|
-
if (centerY <= 0.28)
|
|
321028
|
-
return "top";
|
|
321029
|
-
if (centerX <= 0.35)
|
|
321030
|
-
return "left";
|
|
321031
|
-
if (centerX >= 0.65)
|
|
321032
|
-
return "right";
|
|
321033
|
-
return "center";
|
|
321034
|
-
}
|
|
321035
|
-
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
321036
|
-
const groups = /* @__PURE__ */ new Map();
|
|
321037
|
-
for (const element of elements) {
|
|
321038
|
-
if (!element.navigationProvenance)
|
|
321039
|
-
continue;
|
|
321040
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321041
|
-
if (!candidate)
|
|
321042
|
-
continue;
|
|
321043
|
-
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
321044
|
-
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
321045
|
-
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
321046
|
-
groups.set(groupKey, labels);
|
|
321047
|
-
}
|
|
321048
|
-
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
321049
|
-
}
|
|
321050
|
-
function deriveScreenId(signal) {
|
|
321051
|
-
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
321052
|
-
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
321053
|
-
}
|
|
321054
|
-
function observedElementIdentity(element) {
|
|
321055
|
-
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
321056
|
-
}
|
|
321057
|
-
function mergeObservedElements(previous, current) {
|
|
321058
|
-
const merged = [...current];
|
|
321059
|
-
const seen = new Set(current.map(observedElementIdentity));
|
|
321060
|
-
for (const element of previous) {
|
|
321061
|
-
const identity = observedElementIdentity(element);
|
|
321062
|
-
if (seen.has(identity))
|
|
321063
|
-
continue;
|
|
321064
|
-
seen.add(identity);
|
|
321065
|
-
merged.push(element);
|
|
321066
|
-
}
|
|
321067
|
-
return merged;
|
|
321068
|
-
}
|
|
321069
|
-
function sanitizeElementsForEvidence(elements) {
|
|
321070
|
-
return elements.map((element) => {
|
|
321071
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
321072
|
-
return {
|
|
321073
|
-
...element,
|
|
321074
|
-
...label ? { label } : { label: void 0 },
|
|
321075
|
-
text: void 0
|
|
321076
|
-
};
|
|
321077
|
-
});
|
|
321078
|
-
}
|
|
321079
|
-
var MobileExplorationState = class {
|
|
321080
|
-
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
321081
|
-
screens = /* @__PURE__ */ new Map();
|
|
321082
|
-
/** Observed directed transitions between screens. */
|
|
321083
|
-
transitions = [];
|
|
321084
|
-
/** Transition-announced screens that do not yet have a live observation. */
|
|
321085
|
-
frontier = /* @__PURE__ */ new Map();
|
|
321086
|
-
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
321087
|
-
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
321088
|
-
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
321089
|
-
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
321090
|
-
successfulSafeControlAttemptCount = 0;
|
|
321091
|
-
/** Repeated id-less structural navigation families, grouped by region. */
|
|
321092
|
-
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
321093
|
-
nextSemanticNavigationFamilyId = 1;
|
|
321094
|
-
/** The screen-id of the most recently observed screen. */
|
|
321095
|
-
currentScreenId = null;
|
|
321096
|
-
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
321097
|
-
iteration = 0;
|
|
321098
|
-
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
321099
|
-
lastProgressMark = 0;
|
|
321100
|
-
/** Consecutive iterations observed with no progress. */
|
|
321101
|
-
staleIterations = 0;
|
|
321102
|
-
staleThreshold;
|
|
321103
|
-
constructor(options = {}) {
|
|
321104
|
-
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
321105
|
-
}
|
|
321106
|
-
// -------------------------------------------------------------------------
|
|
321107
|
-
// Observation recording
|
|
321108
|
-
// -------------------------------------------------------------------------
|
|
321109
|
-
/**
|
|
321110
|
-
* Returns true when `signal` identifies a screen this run has never observed
|
|
321111
|
-
* before. Pure read — does not mutate state.
|
|
321112
|
-
*/
|
|
321113
|
-
isNewScreen(signal) {
|
|
321114
|
-
return !this.screens.has(deriveScreenId(signal));
|
|
321115
|
-
}
|
|
321116
|
-
/**
|
|
321117
|
-
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
321118
|
-
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
321119
|
-
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
321120
|
-
* element refresh). A driver observation already contains the structural UI
|
|
321121
|
-
* tree, elements, screenshot association, and screen identity, so it is
|
|
321122
|
-
* captured evidence immediately; the model's capture_screen call only adds
|
|
321123
|
-
* a friendly name/type. Returns the resolved screen-id.
|
|
321124
|
-
*/
|
|
321125
|
-
recordObservation(signal, screen, iteration) {
|
|
321126
|
-
this.iteration = iteration;
|
|
321127
|
-
const screenId = deriveScreenId(signal);
|
|
321128
|
-
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
321129
|
-
const existing = this.screens.get(screenId);
|
|
321130
|
-
if (existing) {
|
|
321131
|
-
this.screens.set(screenId, {
|
|
321132
|
-
...existing,
|
|
321133
|
-
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
321134
|
-
});
|
|
321135
|
-
} else {
|
|
321136
|
-
this.screens.set(screenId, {
|
|
321137
|
-
screenId,
|
|
321138
|
-
firstSeenIteration: iteration,
|
|
321139
|
-
elements: evidenceElements,
|
|
321140
|
-
visited: true
|
|
321141
|
-
});
|
|
321142
|
-
}
|
|
321143
|
-
this.frontier.delete(screenId);
|
|
321144
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321145
|
-
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
321146
|
-
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
321147
|
-
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
321148
|
-
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
321149
|
-
}
|
|
321150
|
-
for (const element of evidenceElements) {
|
|
321151
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321152
|
-
if (!candidate)
|
|
321153
|
-
continue;
|
|
321154
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321155
|
-
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
321156
|
-
controls.set(storedIdentity, {
|
|
321157
|
-
...candidate,
|
|
321158
|
-
identity: storedIdentity,
|
|
321159
|
-
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
321160
|
-
});
|
|
321161
|
-
}
|
|
321162
|
-
if (controls.size > 0)
|
|
321163
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321164
|
-
this.currentScreenId = screenId;
|
|
321165
|
-
this.updateStaleProgress();
|
|
321166
|
-
return screenId;
|
|
321167
|
-
}
|
|
321168
|
-
/**
|
|
321169
|
-
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
321170
|
-
* marks structural evidence visited; this call only resolves a rare pending
|
|
321171
|
-
* transition frontier and preserves callers that annotate explicitly.
|
|
321172
|
-
*/
|
|
321173
|
-
markVisited(screenId) {
|
|
321174
|
-
const state2 = this.screens.get(screenId);
|
|
321175
|
-
if (state2 && !state2.visited) {
|
|
321176
|
-
this.screens.set(screenId, { ...state2, visited: true });
|
|
321177
|
-
}
|
|
321178
|
-
this.frontier.delete(screenId);
|
|
321179
|
-
}
|
|
321180
|
-
/**
|
|
321181
|
-
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
321182
|
-
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
321183
|
-
* failed actions must remain on the breadth frontier.
|
|
321184
|
-
*/
|
|
321185
|
-
noteControlAttempt(screenId, element) {
|
|
321186
|
-
if (!screenId)
|
|
321187
|
-
return false;
|
|
321188
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321189
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321190
|
-
if (candidate) {
|
|
321191
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321192
|
-
const existing = controls.get(storedIdentity);
|
|
321193
|
-
controls.set(storedIdentity, {
|
|
321194
|
-
...candidate,
|
|
321195
|
-
identity: storedIdentity,
|
|
321196
|
-
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
321197
|
-
});
|
|
321198
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321199
|
-
}
|
|
321200
|
-
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
321201
|
-
if (!tappedLabel)
|
|
321202
|
-
return false;
|
|
321203
|
-
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
321204
|
-
const alias = semanticControlAlias(tappedLabel);
|
|
321205
|
-
for (const control of controls.values()) {
|
|
321206
|
-
if (semanticControlAlias(control.label) === alias) {
|
|
321207
|
-
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
321208
|
-
if (control.sharedCoverageIdentity) {
|
|
321209
|
-
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
321210
|
-
}
|
|
321211
|
-
}
|
|
321212
|
-
}
|
|
321213
|
-
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
321214
|
-
this.successfulSafeControlAttemptCount += 1;
|
|
321215
|
-
this.staleIterations = 0;
|
|
321216
|
-
return true;
|
|
321217
|
-
}
|
|
321218
|
-
return false;
|
|
321219
|
-
}
|
|
321220
|
-
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
321221
|
-
annotateScreen(screenId, annotation) {
|
|
321222
|
-
const state2 = this.screens.get(screenId);
|
|
321223
|
-
if (!state2)
|
|
321224
|
-
return;
|
|
321225
|
-
this.screens.set(screenId, {
|
|
321226
|
-
...state2,
|
|
321227
|
-
screenName: annotation.screenName ?? state2.screenName,
|
|
321228
|
-
screenType: annotation.screenType ?? state2.screenType
|
|
321229
|
-
});
|
|
321230
|
-
}
|
|
321231
|
-
/** Attach structurally observed authentication provenance to a screen. */
|
|
321232
|
-
setScreenAuthEvidence(screenId, evidence) {
|
|
321233
|
-
const state2 = this.screens.get(screenId);
|
|
321234
|
-
if (!state2)
|
|
321235
|
-
return;
|
|
321236
|
-
this.screens.set(screenId, {
|
|
321237
|
-
...state2,
|
|
321238
|
-
requiresAuth: evidence.requiresAuth,
|
|
321239
|
-
authProvenance: evidence.authProvenance,
|
|
321240
|
-
sessionState: { ...evidence.sessionState }
|
|
321241
|
-
});
|
|
321242
|
-
}
|
|
321243
|
-
/** Append a free-form observation note to a screen (observations tool). */
|
|
321244
|
-
addScreenObservation(screenId, note) {
|
|
321245
|
-
const state2 = this.screens.get(screenId);
|
|
321246
|
-
if (!state2)
|
|
321247
|
-
return;
|
|
321248
|
-
const normalized = note.trim();
|
|
321249
|
-
if (!normalized)
|
|
321250
|
-
return;
|
|
321251
|
-
const current = state2.observations ?? [];
|
|
321252
|
-
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
321253
|
-
return;
|
|
321254
|
-
}
|
|
321255
|
-
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
321256
|
-
this.screens.set(screenId, { ...state2, observations });
|
|
321257
|
-
}
|
|
321258
|
-
/**
|
|
321259
|
-
* Record a directed transition between two screens. Both endpoints are
|
|
321260
|
-
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
321261
|
-
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
321262
|
-
* `toScreenId` becomes the current screen when the transition starts from the
|
|
321263
|
-
* current screen.
|
|
321264
|
-
*/
|
|
321265
|
-
recordTransition(transition) {
|
|
321266
|
-
let recordedTransition = transition;
|
|
321267
|
-
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
321268
|
-
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
321269
|
-
if (sourceElement) {
|
|
321270
|
-
recordedTransition = {
|
|
321271
|
-
...transition,
|
|
321272
|
-
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
321273
|
-
};
|
|
321274
|
-
}
|
|
321275
|
-
}
|
|
321276
|
-
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
321277
|
-
if (!alreadyRecorded) {
|
|
321278
|
-
this.transitions.push(recordedTransition);
|
|
321279
|
-
}
|
|
321280
|
-
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
321281
|
-
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
321282
|
-
this.frontier.set(endpointId, {
|
|
321283
|
-
screenId: endpointId,
|
|
321284
|
-
source: "transition",
|
|
321285
|
-
discoveredAtIteration: this.iteration
|
|
321286
|
-
});
|
|
321287
|
-
}
|
|
321288
|
-
}
|
|
321289
|
-
if (transition.fromScreenId === this.currentScreenId) {
|
|
321290
|
-
this.currentScreenId = transition.toScreenId;
|
|
321291
|
-
}
|
|
321292
|
-
}
|
|
321293
|
-
// -------------------------------------------------------------------------
|
|
321294
|
-
// Selectors
|
|
321295
|
-
// -------------------------------------------------------------------------
|
|
321296
|
-
/** The current screen-id (most recently observed), or null before any observation. */
|
|
321297
|
-
getCurrentScreenId() {
|
|
321298
|
-
return this.currentScreenId;
|
|
321299
|
-
}
|
|
321300
|
-
/** ScreenState for `screenId`, if known. */
|
|
321301
|
-
getScreen(screenId) {
|
|
321302
|
-
return this.screens.get(screenId);
|
|
321303
|
-
}
|
|
321304
|
-
/** Number of distinct screens observed this run. */
|
|
321305
|
-
getScreenCount() {
|
|
321306
|
-
return this.screens.size;
|
|
321307
|
-
}
|
|
321308
|
-
/** Number of screens backed by a live structural driver observation. */
|
|
321309
|
-
getVisitedScreenCount() {
|
|
321310
|
-
let total = 0;
|
|
321311
|
-
for (const state2 of this.screens.values()) {
|
|
321312
|
-
if (state2.visited)
|
|
321313
|
-
total += 1;
|
|
321314
|
-
}
|
|
321315
|
-
return total;
|
|
321316
|
-
}
|
|
321317
|
-
/** Total elements observed across all known screens (latest snapshot each). */
|
|
321318
|
-
getTotalElementCount() {
|
|
321319
|
-
let total = 0;
|
|
321320
|
-
for (const state2 of this.screens.values()) {
|
|
321321
|
-
total += state2.elements.length;
|
|
321322
|
-
}
|
|
321323
|
-
return total;
|
|
321324
|
-
}
|
|
321325
|
-
/**
|
|
321326
|
-
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
321327
|
-
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
321328
|
-
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
321329
|
-
* which has no mobile analog.
|
|
321330
|
-
*/
|
|
321331
|
-
getFrontier() {
|
|
321332
|
-
return [...this.frontier.values()].filter((item) => {
|
|
321333
|
-
const state2 = this.screens.get(item.screenId);
|
|
321334
|
-
return !state2 || !state2.visited;
|
|
321335
|
-
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
321336
|
-
}
|
|
321337
|
-
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
321338
|
-
getUnexploredSafeNavigationCandidates() {
|
|
321339
|
-
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
321340
|
-
const candidates = [];
|
|
321341
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321342
|
-
const origin = this.screens.get(screenId);
|
|
321343
|
-
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
321344
|
-
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
321345
|
-
continue;
|
|
321346
|
-
for (const control of controls.values()) {
|
|
321347
|
-
const alias = semanticControlAlias(control.label);
|
|
321348
|
-
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
321349
|
-
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
321350
|
-
continue;
|
|
321351
|
-
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
321352
|
-
if (existing) {
|
|
321353
|
-
existing.screenIds.add(screenId);
|
|
321354
|
-
} else {
|
|
321355
|
-
candidates.push({
|
|
321356
|
-
identity: coverageIdentity,
|
|
321357
|
-
label: control.label,
|
|
321358
|
-
kind: control.kind,
|
|
321359
|
-
alias,
|
|
321360
|
-
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
321361
|
-
});
|
|
321362
|
-
}
|
|
321363
|
-
}
|
|
321364
|
-
}
|
|
321365
|
-
return candidates.map((candidate) => ({
|
|
321366
|
-
identity: candidate.identity,
|
|
321367
|
-
label: candidate.label,
|
|
321368
|
-
kind: candidate.kind,
|
|
321369
|
-
screenIds: [...candidate.screenIds]
|
|
321370
|
-
})).sort((a, b) => {
|
|
321371
|
-
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
321372
|
-
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
321373
|
-
if (aCurrent !== bCurrent)
|
|
321374
|
-
return aCurrent ? -1 : 1;
|
|
321375
|
-
return b.screenIds.length - a.screenIds.length;
|
|
321376
|
-
});
|
|
321377
|
-
}
|
|
321378
|
-
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
321379
|
-
getSurveyBreadthCoverage() {
|
|
321380
|
-
const unvisitedScreenIds = this.getFrontier();
|
|
321381
|
-
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
321382
|
-
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
321383
|
-
const missing = [];
|
|
321384
|
-
if (unvisitedScreenIds.length > 0) {
|
|
321385
|
-
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
321386
|
-
}
|
|
321387
|
-
if (unexploredControls.length > 0) {
|
|
321388
|
-
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
321389
|
-
}
|
|
321390
|
-
return {
|
|
321391
|
-
required: true,
|
|
321392
|
-
complete,
|
|
321393
|
-
unvisitedScreenIds,
|
|
321394
|
-
unexploredControls,
|
|
321395
|
-
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
321396
|
-
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
321397
|
-
};
|
|
321398
|
-
}
|
|
321399
|
-
/**
|
|
321400
|
-
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
321401
|
-
* cardinality alone is insufficient: attempting control A can reveal control
|
|
321402
|
-
* B and leave the remaining count unchanged even though real work happened.
|
|
321403
|
-
*/
|
|
321404
|
-
getSurveyBreadthProgressMark() {
|
|
321405
|
-
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
321406
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321407
|
-
for (const control of controls.values()) {
|
|
321408
|
-
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
321409
|
-
}
|
|
321410
|
-
}
|
|
321411
|
-
return [
|
|
321412
|
-
`screens:${this.screens.size}`,
|
|
321413
|
-
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
321414
|
-
`controls:${discoveredControlIdentities.size}`,
|
|
321415
|
-
`transitions:${this.transitions.length}`
|
|
321416
|
-
].join("|");
|
|
321417
|
-
}
|
|
321418
|
-
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
321419
|
-
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
321420
|
-
let best;
|
|
321421
|
-
let bestIntersection = 0;
|
|
321422
|
-
for (const family of families) {
|
|
321423
|
-
let intersection = 0;
|
|
321424
|
-
for (const label of labels) {
|
|
321425
|
-
if (family.labels.has(label))
|
|
321426
|
-
intersection += 1;
|
|
321427
|
-
}
|
|
321428
|
-
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
321429
|
-
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
321430
|
-
best = family;
|
|
321431
|
-
bestIntersection = intersection;
|
|
321432
|
-
}
|
|
321433
|
-
}
|
|
321434
|
-
if (best) {
|
|
321435
|
-
for (const label of labels)
|
|
321436
|
-
best.labels.add(label);
|
|
321437
|
-
return best.id;
|
|
321438
|
-
}
|
|
321439
|
-
const created = {
|
|
321440
|
-
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
321441
|
-
labels: new Set(labels)
|
|
321442
|
-
};
|
|
321443
|
-
families.push(created);
|
|
321444
|
-
this.semanticNavigationFamilies.set(groupKey, families);
|
|
321445
|
-
return created.id;
|
|
321446
|
-
}
|
|
321447
|
-
/** Observed transitions (defensive copy). */
|
|
321448
|
-
getTransitions() {
|
|
321449
|
-
return [...this.transitions];
|
|
321450
|
-
}
|
|
321451
|
-
// -------------------------------------------------------------------------
|
|
321452
|
-
// Stale-progress detection (ported semantics from the web stale guard)
|
|
321453
|
-
// -------------------------------------------------------------------------
|
|
321454
|
-
/**
|
|
321455
|
-
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
321456
|
-
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
321457
|
-
* the stale detector triggers when the mark stops advancing for
|
|
321458
|
-
* `staleThreshold` consecutive iterations.
|
|
321459
|
-
*/
|
|
321460
|
-
getProgressMark() {
|
|
321461
|
-
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
321462
|
-
}
|
|
321463
|
-
updateStaleProgress() {
|
|
321464
|
-
const mark = this.getProgressMark();
|
|
321465
|
-
if (mark > this.lastProgressMark) {
|
|
321466
|
-
this.lastProgressMark = mark;
|
|
321467
|
-
this.staleIterations = 0;
|
|
321468
|
-
} else {
|
|
321469
|
-
this.staleIterations += 1;
|
|
321470
|
-
}
|
|
321471
|
-
}
|
|
321472
|
-
/** Consecutive no-progress iterations observed so far. */
|
|
321473
|
-
getStaleIterations() {
|
|
321474
|
-
return this.staleIterations;
|
|
321475
|
-
}
|
|
321476
|
-
/**
|
|
321477
|
-
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
321478
|
-
* new screen and no new element — the signal the explorer uses to nudge or
|
|
321479
|
-
* finish (mirrors the web explorer's stale guard).
|
|
321480
|
-
*/
|
|
321481
|
-
isStaleProgress() {
|
|
321482
|
-
return this.staleIterations >= this.staleThreshold;
|
|
321483
|
-
}
|
|
321484
|
-
// -------------------------------------------------------------------------
|
|
321485
|
-
// Planner evidence export
|
|
321486
|
-
// -------------------------------------------------------------------------
|
|
321487
|
-
/**
|
|
321488
|
-
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
321489
|
-
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
321490
|
-
* state.
|
|
321491
|
-
*/
|
|
321492
|
-
toScreenGraph() {
|
|
321493
|
-
const screens = /* @__PURE__ */ new Map();
|
|
321494
|
-
for (const [id, state2] of this.screens) {
|
|
321495
|
-
screens.set(id, {
|
|
321496
|
-
...state2,
|
|
321497
|
-
elements: [...state2.elements],
|
|
321498
|
-
observations: state2.observations ? [...state2.observations] : void 0,
|
|
321499
|
-
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
321500
|
-
});
|
|
321501
|
-
}
|
|
321502
|
-
return {
|
|
321503
|
-
screens,
|
|
321504
|
-
transitions: this.transitions.map((t) => ({
|
|
321505
|
-
fromScreenId: t.fromScreenId,
|
|
321506
|
-
toScreenId: t.toScreenId,
|
|
321507
|
-
viaRef: t.viaRef,
|
|
321508
|
-
viaControlIdentity: t.viaControlIdentity,
|
|
321509
|
-
action: t.action
|
|
321510
|
-
}))
|
|
321511
|
-
};
|
|
321512
|
-
}
|
|
321513
|
-
/**
|
|
321514
|
-
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
321515
|
-
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
321516
|
-
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
321517
|
-
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
321518
|
-
* the screen's parsed actionable elements into the platform-neutral
|
|
321519
|
-
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
321520
|
-
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
321521
|
-
* intelligence). `triggersNavigation` is derived from the durable source
|
|
321522
|
-
* element identity captured when the transition happened (with ref-only
|
|
321523
|
-
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
321524
|
-
*/
|
|
321525
|
-
buildInMemoryScreenMap() {
|
|
321526
|
-
const pages = [];
|
|
321527
|
-
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
321528
|
-
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
321529
|
-
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
321530
|
-
for (const transition of this.transitions) {
|
|
321531
|
-
if (transition.viaControlIdentity) {
|
|
321532
|
-
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321533
|
-
set.add(transition.viaControlIdentity);
|
|
321534
|
-
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
321535
|
-
} else if (transition.viaRef) {
|
|
321536
|
-
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321537
|
-
set.add(transition.viaRef);
|
|
321538
|
-
navRefsByScreen.set(transition.fromScreenId, set);
|
|
321539
|
-
}
|
|
321540
|
-
}
|
|
321541
|
-
for (const state2 of ordered) {
|
|
321542
|
-
const page = {
|
|
321543
|
-
route: state2.screenId,
|
|
321544
|
-
screenId: state2.screenId,
|
|
321545
|
-
pageType: state2.screenType ?? "UNKNOWN",
|
|
321546
|
-
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
321547
|
-
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
321548
|
-
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
321549
|
-
}),
|
|
321550
|
-
requiresAuth: state2.requiresAuth ?? false
|
|
321551
|
-
};
|
|
321552
|
-
if (state2.sessionState) {
|
|
321553
|
-
page.sessionState = { ...state2.sessionState };
|
|
321554
|
-
}
|
|
321555
|
-
if (state2.screenName) {
|
|
321556
|
-
page.title = state2.screenName;
|
|
321557
|
-
}
|
|
321558
|
-
if (state2.observations && state2.observations.length > 0) {
|
|
321559
|
-
page.observations = state2.observations.map((note) => ({
|
|
321560
|
-
action: "observe",
|
|
321561
|
-
outcome: note,
|
|
321562
|
-
outcomeType: "SUCCESS"
|
|
321563
|
-
}));
|
|
321564
|
-
}
|
|
321565
|
-
pages.push(page);
|
|
321566
|
-
}
|
|
321567
|
-
return pages;
|
|
321568
|
-
}
|
|
321569
|
-
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
321570
|
-
toScreenMap() {
|
|
321571
|
-
return this.buildInMemoryScreenMap();
|
|
321572
|
-
}
|
|
321573
|
-
};
|
|
321574
|
-
exports2.MobileExplorationState = MobileExplorationState;
|
|
321575
|
-
}
|
|
321576
|
-
});
|
|
321577
|
-
|
|
321578
321736
|
// ../runner-core/dist/services/mobile/mobile-discovery-agent.js
|
|
321579
321737
|
var require_mobile_discovery_agent = __commonJS({
|
|
321580
321738
|
"../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports2) {
|
|
@@ -321870,7 +322028,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321870
322028
|
};
|
|
321871
322029
|
}
|
|
321872
322030
|
if (isSurvey && explore.authCoverage && !explore.authCoverage.complete) {
|
|
321873
|
-
|
|
322031
|
+
log2("Mobile survey continuing to planning with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
|
|
321874
322032
|
}
|
|
321875
322033
|
if (isSurvey && explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
321876
322034
|
log2("Mobile survey continuing to planning with residual breadth gaps: " + (explore.breadthCoverage.reason ?? "Unvisited screens or safe navigation controls remain."));
|
|
@@ -322131,6 +322289,9 @@ Fatal error: ${msg}`);
|
|
|
322131
322289
|
if (explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
322132
322290
|
warnings.push("Discovery retained the captured mobile screen graph with residual navigation gaps: " + (explore.breadthCoverage.reason ?? "some observed controls could not be revisited."));
|
|
322133
322291
|
}
|
|
322292
|
+
if (explore.authCoverage && !explore.authCoverage.complete) {
|
|
322293
|
+
warnings.push("Discovery retained the captured mobile screen graph with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "no authenticated product screen was proven."));
|
|
322294
|
+
}
|
|
322134
322295
|
if (explore.truncated) {
|
|
322135
322296
|
const reasonLabel = explore.truncatedReason === "rate_limit" ? "AI rate-limit" : explore.truncatedReason === "timeout" ? "AI timeout" : "AI error";
|
|
322136
322297
|
warnings.push(`Discovery may be incomplete: exploration aborted by ${reasonLabel}. Re-run after the limit clears for a complete screen graph.`);
|
|
@@ -336640,7 +336801,7 @@ var require_package4 = __commonJS({
|
|
|
336640
336801
|
"package.json"(exports2, module2) {
|
|
336641
336802
|
module2.exports = {
|
|
336642
336803
|
name: "@validate.qa/runner",
|
|
336643
|
-
version: "1.0.
|
|
336804
|
+
version: "1.0.39",
|
|
336644
336805
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336645
336806
|
bin: {
|
|
336646
336807
|
"validate-runner": "dist/cli.js"
|
|
@@ -339895,8 +340056,9 @@ async function activateTargetApp(sessionId, platform3, appId) {
|
|
|
339895
340056
|
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
339896
340057
|
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
339897
340058
|
}
|
|
340059
|
+
var APP_TERMINATE_TIMEOUT_MS = 5e3;
|
|
339898
340060
|
async function terminateTargetApp(sessionId, platform3, appId) {
|
|
339899
|
-
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
340061
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId, timeout: APP_TERMINATE_TIMEOUT_MS }] : [{ appId, timeout: APP_TERMINATE_TIMEOUT_MS }];
|
|
339900
340062
|
await executeMobileScript(sessionId, "mobile: terminateApp", args);
|
|
339901
340063
|
}
|
|
339902
340064
|
async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
@@ -342907,7 +343069,11 @@ var MobileBridgeDriver = class {
|
|
|
342907
343069
|
async relaunch() {
|
|
342908
343070
|
await this.withSessionRecovery("app relaunch", async () => {
|
|
342909
343071
|
const args = this.appLifecycleArgs();
|
|
342910
|
-
|
|
343072
|
+
try {
|
|
343073
|
+
await this.executeScript("mobile: terminateApp", [{ ...args, timeout: 5e3 }]);
|
|
343074
|
+
} catch (err) {
|
|
343075
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
343076
|
+
}
|
|
342911
343077
|
await this.executeScript("mobile: activateApp", [args]);
|
|
342912
343078
|
try {
|
|
342913
343079
|
await waitForColdBootSettle(this.sessionPath);
|