@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.
Files changed (3) hide show
  1. package/dist/cli.js +845 -679
  2. package/dist/cli.mjs +845 -679
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -6218,6 +6218,689 @@ var require_mobile_observation = __commonJS({
6218
6218
  }
6219
6219
  });
6220
6220
 
6221
+ // ../runner-core/dist/services/mobile/mobile-exploration-state.js
6222
+ var require_mobile_exploration_state = __commonJS({
6223
+ "../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports) {
6224
+ "use strict";
6225
+ Object.defineProperty(exports, "__esModule", { value: true });
6226
+ exports.MobileExplorationState = void 0;
6227
+ exports.isDestructiveActionLabel = isDestructiveActionLabel;
6228
+ exports.isDestructiveConfirmationInput = isDestructiveConfirmationInput;
6229
+ exports.stableMobileControlIdentity = stableMobileControlIdentity;
6230
+ exports.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
6231
+ exports.deriveScreenId = deriveScreenId;
6232
+ var mobile_observation_js_1 = require_mobile_observation();
6233
+ var DEFAULT_STALE_THRESHOLD = 4;
6234
+ var MAX_SCREEN_OBSERVATIONS = 50;
6235
+ var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
6236
+ var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
6237
+ 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;
6238
+ var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
6239
+ var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
6240
+ var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
6241
+ var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
6242
+ 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;
6243
+ 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;
6244
+ 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;
6245
+ function isDestructiveActionLabel(label) {
6246
+ if (!label)
6247
+ return false;
6248
+ return DESTRUCTIVE_ACTION_LABEL_RE.test(normalizeControlIdentityPart(label));
6249
+ }
6250
+ function isDestructiveConfirmationInput(value) {
6251
+ if (!value)
6252
+ return false;
6253
+ return DESTRUCTIVE_CONFIRMATION_INPUT_RE.test(value.trim());
6254
+ }
6255
+ function normalizeControlIdentityPart(value) {
6256
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
6257
+ }
6258
+ function semanticControlAlias(label) {
6259
+ return `navigation-label:${normalizeControlIdentityPart(label)}`;
6260
+ }
6261
+ function localControlCoverageIdentity(screenId, identity) {
6262
+ return `screen:${screenId}:${identity}`;
6263
+ }
6264
+ function storedMobileControlIdentity(candidate) {
6265
+ return [
6266
+ candidate.identity,
6267
+ `kind:${candidate.kind}`,
6268
+ `label:${normalizeControlIdentityPart(candidate.label)}`
6269
+ ].join(":");
6270
+ }
6271
+ function isSystemBackNavigationControl(element, label) {
6272
+ if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
6273
+ return true;
6274
+ return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
6275
+ const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
6276
+ return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
6277
+ });
6278
+ }
6279
+ function stableMobileControlIdentity(element) {
6280
+ const role = normalizeControlIdentityPart(element.role || "control");
6281
+ const accessibilityId = element.accessibilityId?.trim();
6282
+ if (accessibilityId)
6283
+ return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
6284
+ const resourceId = element.resourceId?.trim();
6285
+ if (resourceId)
6286
+ return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
6287
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6288
+ if (label)
6289
+ return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
6290
+ return null;
6291
+ }
6292
+ function classifySafeMobileNavigationControl(element) {
6293
+ if (!element.actionable || element.enabled === false)
6294
+ return null;
6295
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6296
+ const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
6297
+ if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
6298
+ return null;
6299
+ if (isSystemBackNavigationControl(element, label))
6300
+ return null;
6301
+ const role = element.role.trim();
6302
+ if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
6303
+ return null;
6304
+ const linkLike = /(?:Link$|^a$)/i.test(role);
6305
+ let kind = null;
6306
+ if (/(?:TabBarItem|\btab\b)/i.test(role))
6307
+ kind = "tab";
6308
+ else if (/menuitem/i.test(role))
6309
+ kind = "menu-item";
6310
+ else if (linkLike && element.navigationProvenance)
6311
+ kind = "link";
6312
+ else if (linkLike)
6313
+ return null;
6314
+ else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
6315
+ kind = "menu-trigger";
6316
+ else if (element.expanded === false)
6317
+ kind = "disclosure";
6318
+ else if (MENU_TRIGGER_LABEL_RE.test(label))
6319
+ kind = "menu-trigger";
6320
+ else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
6321
+ kind = "modal-trigger";
6322
+ else if (element.navigationProvenance)
6323
+ kind = "navigation-button";
6324
+ else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
6325
+ kind = "navigation-button";
6326
+ if (!kind)
6327
+ return null;
6328
+ const identity = stableMobileControlIdentity(element);
6329
+ return identity ? { identity, label, kind } : null;
6330
+ }
6331
+ function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
6332
+ if (!element.navigationProvenance)
6333
+ return void 0;
6334
+ const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
6335
+ if (!durableId && !semanticFamilyId)
6336
+ return void 0;
6337
+ return [
6338
+ "shared-navigation",
6339
+ element.navigationProvenance,
6340
+ !durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
6341
+ candidate.kind,
6342
+ durableId || "semantic-only",
6343
+ normalizeControlIdentityPart(candidate.label)
6344
+ ].filter(Boolean).join(":");
6345
+ }
6346
+ function mobileNavigationRegion(element, windowSize) {
6347
+ if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
6348
+ return "unknown";
6349
+ const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
6350
+ const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
6351
+ if (centerY >= 0.72)
6352
+ return "bottom";
6353
+ if (centerY <= 0.28)
6354
+ return "top";
6355
+ if (centerX <= 0.35)
6356
+ return "left";
6357
+ if (centerX >= 0.65)
6358
+ return "right";
6359
+ return "center";
6360
+ }
6361
+ function buildSemanticNavigationGroups(elements, windowSize) {
6362
+ const groups = /* @__PURE__ */ new Map();
6363
+ for (const element of elements) {
6364
+ if (!element.navigationProvenance)
6365
+ continue;
6366
+ const candidate = classifySafeMobileNavigationControl(element);
6367
+ if (!candidate)
6368
+ continue;
6369
+ const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
6370
+ const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
6371
+ labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
6372
+ groups.set(groupKey, labels);
6373
+ }
6374
+ return new Map([...groups].filter(([, labels]) => labels.size >= 2));
6375
+ }
6376
+ function deriveScreenId(signal) {
6377
+ const namespace = signal.activity?.trim() || signal.bundleId?.trim();
6378
+ return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
6379
+ }
6380
+ function observedElementIdentity(element) {
6381
+ return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
6382
+ }
6383
+ function mergeObservedElements(previous, current) {
6384
+ const merged = [...current];
6385
+ const seen = new Set(current.map(observedElementIdentity));
6386
+ for (const element of previous) {
6387
+ const identity = observedElementIdentity(element);
6388
+ if (seen.has(identity))
6389
+ continue;
6390
+ seen.add(identity);
6391
+ merged.push(element);
6392
+ }
6393
+ return merged;
6394
+ }
6395
+ function sanitizeElementsForEvidence(elements) {
6396
+ return elements.map((element) => {
6397
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
6398
+ return {
6399
+ ...element,
6400
+ ...label ? { label } : { label: void 0 },
6401
+ text: void 0
6402
+ };
6403
+ });
6404
+ }
6405
+ var MobileExplorationState = class {
6406
+ /** screenId -> ScreenState (the mobile analog of routeState). */
6407
+ screens = /* @__PURE__ */ new Map();
6408
+ /** Observed directed transitions between screens. */
6409
+ transitions = [];
6410
+ /** Transition-announced screens that do not yet have a live observation. */
6411
+ frontier = /* @__PURE__ */ new Map();
6412
+ /** Safe navigation controls observed on each screen, keyed by stable identity. */
6413
+ safeControlsByScreen = /* @__PURE__ */ new Map();
6414
+ /** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
6415
+ attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
6416
+ successfulSafeControlAttemptCount = 0;
6417
+ /** Repeated id-less structural navigation families, grouped by region. */
6418
+ semanticNavigationFamilies = /* @__PURE__ */ new Map();
6419
+ nextSemanticNavigationFamilyId = 1;
6420
+ /** The screen-id of the most recently observed screen. */
6421
+ currentScreenId = null;
6422
+ /** Latest iteration recorded (drives stale detection + firstSeen stamps). */
6423
+ iteration = 0;
6424
+ /** Stale detector: progress mark (#screens + #total elements) at last advance. */
6425
+ lastProgressMark = 0;
6426
+ /** Consecutive iterations observed with no progress. */
6427
+ staleIterations = 0;
6428
+ staleThreshold;
6429
+ constructor(options = {}) {
6430
+ this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
6431
+ }
6432
+ // -------------------------------------------------------------------------
6433
+ // Observation recording
6434
+ // -------------------------------------------------------------------------
6435
+ /**
6436
+ * Returns true when `signal` identifies a screen this run has never observed
6437
+ * before. Pure read — does not mutate state.
6438
+ */
6439
+ isNewScreen(signal) {
6440
+ return !this.screens.has(deriveScreenId(signal));
6441
+ }
6442
+ /**
6443
+ * Record an observation of `screen` at `iteration`. Creates a `ScreenState`
6444
+ * on first sight (stamping `firstSeenIteration`), otherwise refreshes the
6445
+ * stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
6446
+ * element refresh). A driver observation already contains the structural UI
6447
+ * tree, elements, screenshot association, and screen identity, so it is
6448
+ * captured evidence immediately; the model's capture_screen call only adds
6449
+ * a friendly name/type. Returns the resolved screen-id.
6450
+ */
6451
+ recordObservation(signal, screen, iteration) {
6452
+ this.iteration = iteration;
6453
+ const screenId = deriveScreenId(signal);
6454
+ const evidenceElements = sanitizeElementsForEvidence(screen.elements);
6455
+ const existing = this.screens.get(screenId);
6456
+ if (existing) {
6457
+ this.screens.set(screenId, {
6458
+ ...existing,
6459
+ elements: mergeObservedElements(existing.elements, evidenceElements)
6460
+ });
6461
+ } else {
6462
+ this.screens.set(screenId, {
6463
+ screenId,
6464
+ firstSeenIteration: iteration,
6465
+ elements: evidenceElements,
6466
+ visited: true
6467
+ });
6468
+ }
6469
+ this.frontier.delete(screenId);
6470
+ const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
6471
+ const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
6472
+ const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
6473
+ for (const [groupKey, labels] of semanticNavigationGroups) {
6474
+ semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
6475
+ }
6476
+ for (const element of evidenceElements) {
6477
+ const candidate = classifySafeMobileNavigationControl(element);
6478
+ if (!candidate)
6479
+ continue;
6480
+ const storedIdentity = storedMobileControlIdentity(candidate);
6481
+ const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
6482
+ controls.set(storedIdentity, {
6483
+ ...candidate,
6484
+ identity: storedIdentity,
6485
+ sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
6486
+ });
6487
+ }
6488
+ if (controls.size > 0)
6489
+ this.safeControlsByScreen.set(screenId, controls);
6490
+ this.currentScreenId = screenId;
6491
+ this.updateStaleProgress();
6492
+ return screenId;
6493
+ }
6494
+ /**
6495
+ * Back-compatible acknowledgement from capture_screen. Observation already
6496
+ * marks structural evidence visited; this call only resolves a rare pending
6497
+ * transition frontier and preserves callers that annotate explicitly.
6498
+ */
6499
+ markVisited(screenId) {
6500
+ const state2 = this.screens.get(screenId);
6501
+ if (state2 && !state2.visited) {
6502
+ this.screens.set(screenId, { ...state2, visited: true });
6503
+ }
6504
+ this.frontier.delete(screenId);
6505
+ }
6506
+ /**
6507
+ * Record a confirmed tap against a safe navigation candidate. Callers invoke
6508
+ * this only after the driver reports success; recovery-unknown, stale-ref, and
6509
+ * failed actions must remain on the breadth frontier.
6510
+ */
6511
+ noteControlAttempt(screenId, element) {
6512
+ if (!screenId)
6513
+ return false;
6514
+ const candidate = classifySafeMobileNavigationControl(element);
6515
+ const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
6516
+ if (candidate) {
6517
+ const storedIdentity = storedMobileControlIdentity(candidate);
6518
+ const existing = controls.get(storedIdentity);
6519
+ controls.set(storedIdentity, {
6520
+ ...candidate,
6521
+ identity: storedIdentity,
6522
+ sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
6523
+ });
6524
+ this.safeControlsByScreen.set(screenId, controls);
6525
+ }
6526
+ const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6527
+ if (!tappedLabel)
6528
+ return false;
6529
+ const before = this.attemptedSafeControlCoverageIdentities.size;
6530
+ const alias = semanticControlAlias(tappedLabel);
6531
+ for (const control of controls.values()) {
6532
+ if (semanticControlAlias(control.label) === alias) {
6533
+ this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
6534
+ if (control.sharedCoverageIdentity) {
6535
+ this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
6536
+ }
6537
+ }
6538
+ }
6539
+ if (this.attemptedSafeControlCoverageIdentities.size > before) {
6540
+ this.successfulSafeControlAttemptCount += 1;
6541
+ this.staleIterations = 0;
6542
+ return true;
6543
+ }
6544
+ return false;
6545
+ }
6546
+ /** Attach a human-friendly name / coarse type to a screen (capture_screen). */
6547
+ annotateScreen(screenId, annotation) {
6548
+ const state2 = this.screens.get(screenId);
6549
+ if (!state2)
6550
+ return;
6551
+ this.screens.set(screenId, {
6552
+ ...state2,
6553
+ screenName: annotation.screenName ?? state2.screenName,
6554
+ screenType: annotation.screenType ?? state2.screenType
6555
+ });
6556
+ }
6557
+ /** Attach structurally observed authentication provenance to a screen. */
6558
+ setScreenAuthEvidence(screenId, evidence) {
6559
+ const state2 = this.screens.get(screenId);
6560
+ if (!state2)
6561
+ return;
6562
+ this.screens.set(screenId, {
6563
+ ...state2,
6564
+ requiresAuth: evidence.requiresAuth,
6565
+ authProvenance: evidence.authProvenance,
6566
+ sessionState: { ...evidence.sessionState }
6567
+ });
6568
+ }
6569
+ /** Append a free-form observation note to a screen (observations tool). */
6570
+ addScreenObservation(screenId, note) {
6571
+ const state2 = this.screens.get(screenId);
6572
+ if (!state2)
6573
+ return;
6574
+ const normalized = note.trim();
6575
+ if (!normalized)
6576
+ return;
6577
+ const current = state2.observations ?? [];
6578
+ if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
6579
+ return;
6580
+ }
6581
+ const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
6582
+ this.screens.set(screenId, { ...state2, observations });
6583
+ }
6584
+ /**
6585
+ * Record a directed transition between two screens. Both endpoints are
6586
+ * ensured to exist on the frontier if not yet observed (mirrors the web
6587
+ * reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
6588
+ * `toScreenId` becomes the current screen when the transition starts from the
6589
+ * current screen.
6590
+ */
6591
+ recordTransition(transition) {
6592
+ let recordedTransition = transition;
6593
+ if (!transition.viaControlIdentity && transition.viaRef) {
6594
+ const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
6595
+ if (sourceElement) {
6596
+ recordedTransition = {
6597
+ ...transition,
6598
+ viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
6599
+ };
6600
+ }
6601
+ }
6602
+ const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
6603
+ if (!alreadyRecorded) {
6604
+ this.transitions.push(recordedTransition);
6605
+ }
6606
+ for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
6607
+ if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
6608
+ this.frontier.set(endpointId, {
6609
+ screenId: endpointId,
6610
+ source: "transition",
6611
+ discoveredAtIteration: this.iteration
6612
+ });
6613
+ }
6614
+ }
6615
+ if (transition.fromScreenId === this.currentScreenId) {
6616
+ this.currentScreenId = transition.toScreenId;
6617
+ }
6618
+ }
6619
+ // -------------------------------------------------------------------------
6620
+ // Selectors
6621
+ // -------------------------------------------------------------------------
6622
+ /** The current screen-id (most recently observed), or null before any observation. */
6623
+ getCurrentScreenId() {
6624
+ return this.currentScreenId;
6625
+ }
6626
+ /** ScreenState for `screenId`, if known. */
6627
+ getScreen(screenId) {
6628
+ return this.screens.get(screenId);
6629
+ }
6630
+ /** Number of distinct screens observed this run. */
6631
+ getScreenCount() {
6632
+ return this.screens.size;
6633
+ }
6634
+ /** Number of screens backed by a live structural driver observation. */
6635
+ getVisitedScreenCount() {
6636
+ let total = 0;
6637
+ for (const state2 of this.screens.values()) {
6638
+ if (state2.visited)
6639
+ total += 1;
6640
+ }
6641
+ return total;
6642
+ }
6643
+ /** Total elements observed across all known screens (latest snapshot each). */
6644
+ getTotalElementCount() {
6645
+ let total = 0;
6646
+ for (const state2 of this.screens.values()) {
6647
+ total += state2.elements.length;
6648
+ }
6649
+ return total;
6650
+ }
6651
+ /**
6652
+ * Frontier screen-ids ranked by recency (most recently discovered first),
6653
+ * excluding any screen already visited. Mirrors the web `getRankedFrontier`
6654
+ * intent (filter visited, rank by recency) — without URL prefix similarity,
6655
+ * which has no mobile analog.
6656
+ */
6657
+ getFrontier() {
6658
+ return [...this.frontier.values()].filter((item) => {
6659
+ const state2 = this.screens.get(item.screenId);
6660
+ return !state2 || !state2.visited;
6661
+ }).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
6662
+ }
6663
+ /** Safe navigation candidates not yet attempted, deduped across shared chrome. */
6664
+ getUnexploredSafeNavigationCandidates() {
6665
+ const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
6666
+ const candidates = [];
6667
+ for (const [screenId, controls] of this.safeControlsByScreen) {
6668
+ const origin = this.screens.get(screenId);
6669
+ const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
6670
+ if (authenticatedProductObserved && originIsConfirmedPublicEntry)
6671
+ continue;
6672
+ for (const control of controls.values()) {
6673
+ const alias = semanticControlAlias(control.label);
6674
+ const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
6675
+ if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
6676
+ continue;
6677
+ const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
6678
+ if (existing) {
6679
+ existing.screenIds.add(screenId);
6680
+ } else {
6681
+ candidates.push({
6682
+ identity: coverageIdentity,
6683
+ label: control.label,
6684
+ kind: control.kind,
6685
+ alias,
6686
+ screenIds: /* @__PURE__ */ new Set([screenId])
6687
+ });
6688
+ }
6689
+ }
6690
+ }
6691
+ return candidates.map((candidate) => ({
6692
+ identity: candidate.identity,
6693
+ label: candidate.label,
6694
+ kind: candidate.kind,
6695
+ screenIds: [...candidate.screenIds]
6696
+ })).sort((a, b) => {
6697
+ const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
6698
+ const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
6699
+ if (aCurrent !== bCurrent)
6700
+ return aCurrent ? -1 : 1;
6701
+ return b.screenIds.length - a.screenIds.length;
6702
+ });
6703
+ }
6704
+ /** Harness-owned survey completion floor, independent of model assertions. */
6705
+ getSurveyBreadthCoverage() {
6706
+ const unvisitedScreenIds = this.getFrontier();
6707
+ const unexploredControls = this.getUnexploredSafeNavigationCandidates();
6708
+ const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
6709
+ const missing = [];
6710
+ if (unvisitedScreenIds.length > 0) {
6711
+ missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
6712
+ }
6713
+ if (unexploredControls.length > 0) {
6714
+ missing.push(`${unexploredControls.length} safe navigation control(s)`);
6715
+ }
6716
+ return {
6717
+ required: true,
6718
+ complete,
6719
+ unvisitedScreenIds,
6720
+ unexploredControls,
6721
+ attemptedControlCount: this.successfulSafeControlAttemptCount,
6722
+ ...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
6723
+ };
6724
+ }
6725
+ /**
6726
+ * Monotonic structural progress token for adaptive survey leases. Frontier
6727
+ * cardinality alone is insufficient: attempting control A can reveal control
6728
+ * B and leave the remaining count unchanged even though real work happened.
6729
+ */
6730
+ getSurveyBreadthProgressMark() {
6731
+ const discoveredControlIdentities = /* @__PURE__ */ new Set();
6732
+ for (const [screenId, controls] of this.safeControlsByScreen) {
6733
+ for (const control of controls.values()) {
6734
+ discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
6735
+ }
6736
+ }
6737
+ return [
6738
+ `screens:${this.screens.size}`,
6739
+ `attempts:${this.successfulSafeControlAttemptCount}`,
6740
+ `controls:${discoveredControlIdentities.size}`,
6741
+ `transitions:${this.transitions.length}`
6742
+ ].join("|");
6743
+ }
6744
+ resolveSemanticNavigationFamilyId(groupKey, labels) {
6745
+ const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
6746
+ let best;
6747
+ let bestIntersection = 0;
6748
+ for (const family of families) {
6749
+ let intersection = 0;
6750
+ for (const label of labels) {
6751
+ if (family.labels.has(label))
6752
+ intersection += 1;
6753
+ }
6754
+ const overlap = intersection / Math.min(labels.size, family.labels.size);
6755
+ if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
6756
+ best = family;
6757
+ bestIntersection = intersection;
6758
+ }
6759
+ }
6760
+ if (best) {
6761
+ for (const label of labels)
6762
+ best.labels.add(label);
6763
+ return best.id;
6764
+ }
6765
+ const created = {
6766
+ id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
6767
+ labels: new Set(labels)
6768
+ };
6769
+ families.push(created);
6770
+ this.semanticNavigationFamilies.set(groupKey, families);
6771
+ return created.id;
6772
+ }
6773
+ /** Observed transitions (defensive copy). */
6774
+ getTransitions() {
6775
+ return [...this.transitions];
6776
+ }
6777
+ // -------------------------------------------------------------------------
6778
+ // Stale-progress detection (ported semantics from the web stale guard)
6779
+ // -------------------------------------------------------------------------
6780
+ /**
6781
+ * Progress mark: distinct screens weighted heavily, plus total elements seen.
6782
+ * A run advances this mark whenever it finds a new screen OR new elements;
6783
+ * the stale detector triggers when the mark stops advancing for
6784
+ * `staleThreshold` consecutive iterations.
6785
+ */
6786
+ getProgressMark() {
6787
+ return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
6788
+ }
6789
+ updateStaleProgress() {
6790
+ const mark = this.getProgressMark();
6791
+ if (mark > this.lastProgressMark) {
6792
+ this.lastProgressMark = mark;
6793
+ this.staleIterations = 0;
6794
+ } else {
6795
+ this.staleIterations += 1;
6796
+ }
6797
+ }
6798
+ /** Consecutive no-progress iterations observed so far. */
6799
+ getStaleIterations() {
6800
+ return this.staleIterations;
6801
+ }
6802
+ /**
6803
+ * True when the run has gone `staleThreshold` consecutive iterations with no
6804
+ * new screen and no new element — the signal the explorer uses to nudge or
6805
+ * finish (mirrors the web explorer's stale guard).
6806
+ */
6807
+ isStaleProgress() {
6808
+ return this.staleIterations >= this.staleThreshold;
6809
+ }
6810
+ // -------------------------------------------------------------------------
6811
+ // Planner evidence export
6812
+ // -------------------------------------------------------------------------
6813
+ /**
6814
+ * Snapshot the screen-graph as a plain object (frozen mobile-types
6815
+ * `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
6816
+ * state.
6817
+ */
6818
+ toScreenGraph() {
6819
+ const screens = /* @__PURE__ */ new Map();
6820
+ for (const [id, state2] of this.screens) {
6821
+ screens.set(id, {
6822
+ ...state2,
6823
+ elements: [...state2.elements],
6824
+ observations: state2.observations ? [...state2.observations] : void 0,
6825
+ sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
6826
+ });
6827
+ }
6828
+ return {
6829
+ screens,
6830
+ transitions: this.transitions.map((t) => ({
6831
+ fromScreenId: t.fromScreenId,
6832
+ toScreenId: t.toScreenId,
6833
+ viaRef: t.viaRef,
6834
+ viaControlIdentity: t.viaControlIdentity,
6835
+ action: t.action
6836
+ }))
6837
+ };
6838
+ }
6839
+ /**
6840
+ * Build the planner evidence shape: one `CollectedPageData` per screen, with
6841
+ * the screen-id carried in BOTH `route` (the on-the-wire positional field the
6842
+ * server keys screenshots by) AND the explicit `screenId` field (per the
6843
+ * Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
6844
+ * the screen's parsed actionable elements into the platform-neutral
6845
+ * `CollectedPageElement[]` shape the server persists as SiteElements — without
6846
+ * this, mobile screens land with zero elements (coverage always 0%, no element
6847
+ * intelligence). `triggersNavigation` is derived from the durable source
6848
+ * element identity captured when the transition happened (with ref-only
6849
+ * fallback for legacy transitions). Insertion order matches first-seen order.
6850
+ */
6851
+ buildInMemoryScreenMap() {
6852
+ const pages = [];
6853
+ const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
6854
+ const navRefsByScreen = /* @__PURE__ */ new Map();
6855
+ const navIdentitiesByScreen = /* @__PURE__ */ new Map();
6856
+ for (const transition of this.transitions) {
6857
+ if (transition.viaControlIdentity) {
6858
+ const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
6859
+ set.add(transition.viaControlIdentity);
6860
+ navIdentitiesByScreen.set(transition.fromScreenId, set);
6861
+ } else if (transition.viaRef) {
6862
+ const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
6863
+ set.add(transition.viaRef);
6864
+ navRefsByScreen.set(transition.fromScreenId, set);
6865
+ }
6866
+ }
6867
+ for (const state2 of ordered) {
6868
+ const page = {
6869
+ route: state2.screenId,
6870
+ screenId: state2.screenId,
6871
+ pageType: state2.screenType ?? "UNKNOWN",
6872
+ elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
6873
+ navigationRefs: navRefsByScreen.get(state2.screenId),
6874
+ navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
6875
+ }),
6876
+ requiresAuth: state2.requiresAuth ?? false
6877
+ };
6878
+ if (state2.sessionState) {
6879
+ page.sessionState = { ...state2.sessionState };
6880
+ }
6881
+ if (state2.screenName) {
6882
+ page.title = state2.screenName;
6883
+ }
6884
+ if (state2.observations && state2.observations.length > 0) {
6885
+ page.observations = state2.observations.map((note) => ({
6886
+ action: "observe",
6887
+ outcome: note,
6888
+ outcomeType: "SUCCESS"
6889
+ }));
6890
+ }
6891
+ pages.push(page);
6892
+ }
6893
+ return pages;
6894
+ }
6895
+ /** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
6896
+ toScreenMap() {
6897
+ return this.buildInMemoryScreenMap();
6898
+ }
6899
+ };
6900
+ exports.MobileExplorationState = MobileExplorationState;
6901
+ }
6902
+ });
6903
+
6221
6904
  // ../runner-core/dist/services/mobile/mobile-keyboard.js
6222
6905
  var require_mobile_keyboard = __commonJS({
6223
6906
  "../runner-core/dist/services/mobile/mobile-keyboard.js"(exports) {
@@ -63936,6 +64619,7 @@ var require_mobile_explorer = __commonJS({
63936
64619
  var ai_retry_js_1 = require_ai_retry();
63937
64620
  var mobile_source_parser_js_1 = require_mobile_source_parser();
63938
64621
  var mobile_observation_js_1 = require_mobile_observation();
64622
+ var mobile_exploration_state_js_1 = require_mobile_exploration_state();
63939
64623
  var mobile_boundary_js_1 = require_mobile_boundary();
63940
64624
  var mobile_keyboard_js_1 = require_mobile_keyboard();
63941
64625
  var mobile_auth_coverage_js_1 = require_mobile_auth_coverage();
@@ -64293,6 +64977,10 @@ var require_mobile_explorer = __commonJS({
64293
64977
  return void 0;
64294
64978
  return normalized;
64295
64979
  }
64980
+ function destructiveActionRefusal(label, kind = "tap") {
64981
+ const what = kind === "confirmation" ? `Typing "${label}" is a destructive confirmation` : `Tapping "${label}" is an irreversible, high-stakes destructive action`;
64982
+ 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.`;
64983
+ }
64296
64984
  function hybridElementForEvidence(element, credentials) {
64297
64985
  const id = safeHybridIdentity(element.id, credentials);
64298
64986
  const name = safeHybridIdentity(element.name, credentials);
@@ -64915,7 +65603,7 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
64915
65603
  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."}
64916
65604
  4. Use mobile_relaunch for a clean, hermetic restart before testing a distinct flow that needs fresh state.
64917
65605
  5. mobile_swipe to scroll; mobile_back to return.
64918
- 6. Do NOT trigger irreversible side effects (real payments, sending messages/invites to others). Note them instead.
65606
+ 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.
64919
65607
  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.
64920
65608
 
64921
65609
  Max iterations: ${maxIterations}. You will be warned near the end.`;
@@ -65780,6 +66468,11 @@ Exploration complete: ${summary}`);
65780
66468
  if (!driver.webTap) {
65781
66469
  return { ok: false, error: "This driver does not support hybrid WEBVIEW taps." };
65782
66470
  }
66471
+ const hybridTapLabel = hybridElementForEvidence(hybrid, redactionCredentials).label ?? "";
66472
+ if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(hybridTapLabel)) {
66473
+ log2(` \u26A0 Refused destructive tap on "${hybridTapLabel}" (account/data-deletion class); noting instead.`);
66474
+ return { ok: false, error: destructiveActionRefusal(hybridTapLabel) };
66475
+ }
65783
66476
  const authSurfaceBeforeTap2 = currentAuthSurface();
65784
66477
  const fromScreenId2 = state2.getCurrentScreenId();
65785
66478
  const actionResult2 = await driver.webTap(hybrid.webviewContext, hybrid.css);
@@ -65810,6 +66503,11 @@ Exploration complete: ${summary}`);
65810
66503
  if (!resolved) {
65811
66504
  return { ok: false, error: `Unknown element ref "${String(toolArgs.ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.` };
65812
66505
  }
66506
+ const nativeTapLabel = resolved.element.label ?? resolved.element.text ?? resolved.element.accessibilityId ?? "";
66507
+ if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(nativeTapLabel)) {
66508
+ log2(` \u26A0 Refused destructive tap on "${nativeTapLabel}" (account/data-deletion class); noting instead.`);
66509
+ return { ok: false, error: destructiveActionRefusal(nativeTapLabel) };
66510
+ }
65813
66511
  const fromScreenId = state2.getCurrentScreenId();
65814
66512
  const boundaryBeforeAction = currentBoundary();
65815
66513
  const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
@@ -65850,6 +66548,10 @@ Exploration complete: ${summary}`);
65850
66548
  // ----- Type -----
65851
66549
  case "mobile_type": {
65852
66550
  const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
66551
+ if ((0, mobile_exploration_state_js_1.isDestructiveConfirmationInput)(value)) {
66552
+ log2(` \u26A0 Refused destructive confirmation input "${value}"; noting instead.`);
66553
+ return { ok: false, error: destructiveActionRefusal(value, "confirmation") };
66554
+ }
65853
66555
  const allowedLiteral = inbox ? inbox.state.email : "";
65854
66556
  for (const [key, secret] of Object.entries(credentials)) {
65855
66557
  if (!secret || secret.length < 4)
@@ -70559,7 +71261,7 @@ var require_mobile_generator = __commonJS({
70559
71261
  - Re-snapshot before every assertion so the ref is fresh.
70560
71262
  - Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
70561
71263
  - 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.
70562
- - Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
71264
+ - 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.
70563
71265
  - 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.
70564
71266
  - 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.
70565
71267
  - 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.
@@ -79232,6 +79934,34 @@ var require_anchor_harvester = __commonJS({
79232
79934
  }
79233
79935
  });
79234
79936
 
79937
+ // ../runner-core/dist/services/discovery-lint-flags.js
79938
+ var require_discovery_lint_flags = __commonJS({
79939
+ "../runner-core/dist/services/discovery-lint-flags.js"(exports) {
79940
+ "use strict";
79941
+ Object.defineProperty(exports, "__esModule", { value: true });
79942
+ exports.structuralLocatorLintMode = structuralLocatorLintMode;
79943
+ exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
79944
+ exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
79945
+ function strictOn(value) {
79946
+ return value === "true" || value === "1";
79947
+ }
79948
+ function structuralLocatorLintMode() {
79949
+ const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
79950
+ if (v === "warn")
79951
+ return "warn";
79952
+ if (v === "true" || v === "1" || v === "enforce")
79953
+ return "enforce";
79954
+ return "off";
79955
+ }
79956
+ function isPwMethodPreflightEnabled() {
79957
+ return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
79958
+ }
79959
+ function isVqaLoginDedupeEnabled() {
79960
+ return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
79961
+ }
79962
+ }
79963
+ });
79964
+
79235
79965
  // ../runner-core/dist/services/auth-recipe.js
79236
79966
  var require_auth_recipe = __commonJS({
79237
79967
  "../runner-core/dist/services/auth-recipe.js"(exports) {
@@ -79254,9 +79984,11 @@ var require_auth_recipe = __commonJS({
79254
79984
  exports.extractAuthRegion = extractAuthRegion;
79255
79985
  exports.stripAuthRegions = stripAuthRegions;
79256
79986
  exports.ensureExpectImported = ensureExpectImported;
79987
+ exports.ensureLoginCall = ensureLoginCall;
79257
79988
  exports.upsertAuthBlock = upsertAuthBlock;
79258
79989
  exports.buildHealAuthStamp = buildHealAuthStamp;
79259
79990
  var crypto_1 = __require("crypto");
79991
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
79260
79992
  exports.AUTH_MARKER_VERSION = "v1";
79261
79993
  var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
79262
79994
  var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
@@ -79520,10 +80252,13 @@ ${tail}`;
79520
80252
  var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
79521
80253
  var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
79522
80254
  function ensureLoginCall(code) {
79523
- const matches = [...code.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
79524
- if (matches.length === 0)
80255
+ if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
79525
80256
  return code;
79526
80257
  let result = code;
80258
+ if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
80259
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
80260
+ }
80261
+ const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
79527
80262
  for (let i = matches.length - 1; i >= 0; i--) {
79528
80263
  const m = matches[i];
79529
80264
  if (m.index === void 0)
@@ -83415,7 +84150,7 @@ var require_discover_generator = __commonJS({
83415
84150
  "../runner-core/dist/services/discover-generator.js"(exports) {
83416
84151
  "use strict";
83417
84152
  Object.defineProperty(exports, "__esModule", { value: true });
83418
- exports.MIN_BUG_DESCRIPTION_LENGTH = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
84153
+ exports.MIN_BUG_DESCRIPTION_LENGTH = exports.VALID_PLAYWRIGHT_GETBY_METHODS = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
83419
84154
  exports.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
83420
84155
  exports.detectScenarioSignals = detectScenarioSignals;
83421
84156
  exports.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
@@ -83438,6 +84173,7 @@ var require_discover_generator = __commonJS({
83438
84173
  exports.upgradePositionalProvenLines = upgradePositionalProvenLines;
83439
84174
  exports.synthesizeRefActionLines = synthesizeRefActionLines;
83440
84175
  exports.renderSessionRecording = renderSessionRecording;
84176
+ exports.scanFragileStructuralLocators = scanFragileStructuralLocators;
83441
84177
  exports.validateFinalActionFidelity = validateFinalActionFidelity;
83442
84178
  exports.resolveSuspectedBugVerdict = resolveSuspectedBugVerdict;
83443
84179
  exports.buildScenarioSystemPrompt = buildScenarioSystemPrompt;
@@ -83465,6 +84201,7 @@ var require_discover_generator = __commonJS({
83465
84201
  var auth_recipe_js_1 = require_auth_recipe();
83466
84202
  var auth_replay_js_1 = require_auth_replay();
83467
84203
  var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
84204
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
83468
84205
  exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
83469
84206
  "browser_install",
83470
84207
  "browser_close",
@@ -84189,6 +84926,15 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
84189
84926
  return assembled.join(separator);
84190
84927
  }
84191
84928
  var SENSITIVE_ENV_KEY_RE = /email|password|token|secret|api[_-]?key|access[_-]?key|username|user(name)?|bearer|session|cookie|\bpat\b|refresh|jwt/i;
84929
+ exports.VALID_PLAYWRIGHT_GETBY_METHODS = /* @__PURE__ */ new Set([
84930
+ "getByRole",
84931
+ "getByText",
84932
+ "getByLabel",
84933
+ "getByPlaceholder",
84934
+ "getByTestId",
84935
+ "getByAltText",
84936
+ "getByTitle"
84937
+ ]);
84192
84938
  var VALID_PLAYWRIGHT_MATCHERS = /* @__PURE__ */ new Set([
84193
84939
  // Locator assertions
84194
84940
  "toBeAttached",
@@ -84676,6 +85422,16 @@ ${testOpen}`;
84676
85422
  return false;
84677
85423
  }
84678
85424
  }
85425
+ if ((0, discovery_lint_flags_js_1.isPwMethodPreflightEnabled)()) {
85426
+ const getByCalls = codeForMatcherScan.match(/\.getBy[A-Za-z0-9_]*\s*\(/g) ?? [];
85427
+ for (const call of getByCalls) {
85428
+ const methodName = call.replace(/[\s(]+$/, "").slice(1);
85429
+ if (!exports.VALID_PLAYWRIGHT_GETBY_METHODS.has(methodName)) {
85430
+ logs2?.push(` \u2717 Rejected "${t.name}": invalid Playwright locator method "${methodName}" (not a real getBy* API \u2014 likely hallucinated)`);
85431
+ return false;
85432
+ }
85433
+ }
85434
+ }
84679
85435
  if (code.includes("// TODO")) {
84680
85436
  logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
84681
85437
  t.tags = t.tags.filter((tag) => tag !== "@verified");
@@ -85789,6 +86545,35 @@ ${testOpen}`;
85789
86545
  }
85790
86546
  return null;
85791
86547
  }
86548
+ var STRUCTURAL_ACTION_RE = /\.(?:click|dblclick|tap|fill|type|pressSequentially|press|selectOption|hover|check|uncheck|setChecked|setInputFiles|dragTo|focus|clear)\s*\(/;
86549
+ var POSITIONAL_PSEUDO_RE = /:(?:last-child|first-child|nth-child|nth-of-type|nth-last-child|nth-last-of-type)\b/i;
86550
+ var TABLE_TOKEN_RE = /\b(?:tr|td|th|tbody|thead)\b/i;
86551
+ var ROW_ANCHOR_RE = /getByRole\s*\(\s*['"`]row['"`]|getByTestId|data-testid|data-test\b|\[role\s*=\s*['"`]row['"`]\]/i;
86552
+ var LOCATOR_ARG_RE = /\.locator\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
86553
+ function scanFragileStructuralLocators(code) {
86554
+ const findings = [];
86555
+ const scanCode = code.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
86556
+ for (const rawLine of scanCode.split("\n")) {
86557
+ const line = rawLine.replace(/\/\/.*$/, "");
86558
+ if (!STRUCTURAL_ACTION_RE.test(line))
86559
+ continue;
86560
+ if (ROW_ANCHOR_RE.test(line))
86561
+ continue;
86562
+ LOCATOR_ARG_RE.lastIndex = 0;
86563
+ let m;
86564
+ while ((m = LOCATOR_ARG_RE.exec(line)) !== null) {
86565
+ const css = m[2];
86566
+ if (POSITIONAL_PSEUDO_RE.test(css) && TABLE_TOKEN_RE.test(css)) {
86567
+ findings.push({
86568
+ line: rawLine.trim().slice(0, 200),
86569
+ 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>' }).`
86570
+ });
86571
+ break;
86572
+ }
86573
+ }
86574
+ }
86575
+ return findings;
86576
+ }
85792
86577
  function validateFinalActionFidelity(playwrightCode, actionLedger, credentials, opts) {
85793
86578
  const ledgerSigs = [];
85794
86579
  for (const entry of actionLedger) {
@@ -86228,6 +87013,7 @@ ${tailBlock}`;
86228
87013
  let actionRevisionAttempted = false;
86229
87014
  let ambiguityRevisionAttempted = false;
86230
87015
  let strengthRevisionAttempted = false;
87016
+ let structuralRevisionAttempted = false;
86231
87017
  let lastRenderedLedgerEntryCount = 0;
86232
87018
  const successfulVerifications = [];
86233
87019
  const actionLedger = [];
@@ -86498,6 +87284,33 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
86498
87284
  });
86499
87285
  continue;
86500
87286
  }
87287
+ const structuralLintMode = (0, discovery_lint_flags_js_1.structuralLocatorLintMode)();
87288
+ const structuralFindings = structuralLintMode !== "off" ? scanFragileStructuralLocators(validated[0].playwrightCode) : [];
87289
+ if (structuralLintMode === "warn" && structuralFindings.length > 0) {
87290
+ logs2.push(` Structural-locator gate [warn]: ${structuralFindings.length} positional table-row selector(s) \u2014 ${structuralFindings.map((f) => sanitizeLedgerText(f.line)).join(" | ").slice(0, 300)}`);
87291
+ }
87292
+ if (structuralLintMode === "enforce" && structuralFindings.length > 0 && !structuralRevisionAttempted) {
87293
+ structuralRevisionAttempted = true;
87294
+ const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
87295
+ if (renderedLedger)
87296
+ lastRenderedLedgerEntryCount = actionLedger.length;
87297
+ logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) found \u2014 requesting one revision (recording attached: ${renderedLedger ? "yes" : "no"})`);
87298
+ const structuralPayload = {
87299
+ acknowledged: false,
87300
+ valid: false,
87301
+ revisionRequired: true,
87302
+ reason: "fragile_structural_locator",
87303
+ 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.",
87304
+ offendingLocators: structuralFindings.map((f) => ({
87305
+ line: sanitizeLedgerText(f.line),
87306
+ reason: f.reason
87307
+ }))
87308
+ };
87309
+ if (renderedLedger)
87310
+ structuralPayload.sessionRecording = renderedLedger;
87311
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(structuralPayload) });
87312
+ continue;
87313
+ }
86501
87314
  const ambiguityGateEval = evaluateAmbiguityGate(validated[0].playwrightCode, ambiguityRevisionAttempted, iteration < MAX_SCENARIO_ITERATIONS);
86502
87315
  const ambiguousLocators = ambiguityGateEval.findings;
86503
87316
  if (ambiguityGateEval.fire) {
@@ -86594,12 +87407,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86594
87407
  const strengthSeverity = strengthVerdict?.severity ?? "none";
86595
87408
  const hasResidualViolations = qualityCheck.offendingLines.length > 0;
86596
87409
  const hasResidualActionDrift = actionCheck.offendingActions.length > 0;
87410
+ const hasResidualStructural = structuralLintMode === "enforce" && structuralFindings.length > 0;
86597
87411
  if (hasResidualViolations) {
86598
87412
  logs2.push(` Assertion quality gate: violations remain after revision \u2014 downgrading to @needs-review`);
86599
87413
  }
86600
87414
  if (hasResidualActionDrift) {
86601
87415
  logs2.push(` Action fidelity gate: ${actionCheck.offendingActions.length} action drift(s) remain after revision \u2014 downgrading to @needs-review`);
86602
87416
  }
87417
+ if (hasResidualStructural) {
87418
+ logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) remain after revision \u2014 downgrading to @needs-review`);
87419
+ }
86603
87420
  let actualVerified = verified;
86604
87421
  if (verified && verifyCallCount === 0) {
86605
87422
  actualVerified = false;
@@ -86652,6 +87469,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86652
87469
  residual: ambiguousLocators.length,
86653
87470
  lines: ambiguousLocators.map((a) => sanitizeLedgerText(a.line))
86654
87471
  },
87472
+ // G2 — omitted entirely when off so the off-path healingContext is
87473
+ // byte-identical; present in warn (measure) + enforce (act) modes.
87474
+ ...structuralLintMode !== "off" ? {
87475
+ structuralGate: {
87476
+ mode: structuralLintMode,
87477
+ fired: structuralRevisionAttempted,
87478
+ residual: structuralFindings.length,
87479
+ lines: structuralFindings.map((f) => sanitizeLedgerText(f.line))
87480
+ }
87481
+ } : {},
86655
87482
  // Durable marker the server promote-seam (failure-analyzer.checkTestStability)
86656
87483
  // reads to decide ACTIVE vs QUARANTINED(@bug-found) vs hold-DRAFT. Omitted when
86657
87484
  // the gate errored (fail-open) so a missing marker is treated as "promote".
@@ -86677,7 +87504,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86677
87504
  verified: actualVerified,
86678
87505
  healingContext: { ...baseHealingContext, gateActivity }
86679
87506
  };
86680
- if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes) && !result.tags.includes("@needs-review")) {
87507
+ if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes || hasResidualStructural) && !result.tags.includes("@needs-review")) {
86681
87508
  result.tags.push("@needs-review");
86682
87509
  }
86683
87510
  if (actualVerified) {
@@ -320911,675 +321738,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
320911
321738
  }
320912
321739
  });
320913
321740
 
320914
- // ../runner-core/dist/services/mobile/mobile-exploration-state.js
320915
- var require_mobile_exploration_state = __commonJS({
320916
- "../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports) {
320917
- "use strict";
320918
- Object.defineProperty(exports, "__esModule", { value: true });
320919
- exports.MobileExplorationState = void 0;
320920
- exports.stableMobileControlIdentity = stableMobileControlIdentity;
320921
- exports.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
320922
- exports.deriveScreenId = deriveScreenId;
320923
- var mobile_observation_js_1 = require_mobile_observation();
320924
- var DEFAULT_STALE_THRESHOLD = 4;
320925
- var MAX_SCREEN_OBSERVATIONS = 50;
320926
- var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
320927
- var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
320928
- 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;
320929
- var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
320930
- var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
320931
- var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
320932
- var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
320933
- 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;
320934
- function normalizeControlIdentityPart(value) {
320935
- return value.trim().replace(/\s+/g, " ").toLowerCase();
320936
- }
320937
- function semanticControlAlias(label) {
320938
- return `navigation-label:${normalizeControlIdentityPart(label)}`;
320939
- }
320940
- function localControlCoverageIdentity(screenId, identity) {
320941
- return `screen:${screenId}:${identity}`;
320942
- }
320943
- function storedMobileControlIdentity(candidate) {
320944
- return [
320945
- candidate.identity,
320946
- `kind:${candidate.kind}`,
320947
- `label:${normalizeControlIdentityPart(candidate.label)}`
320948
- ].join(":");
320949
- }
320950
- function isSystemBackNavigationControl(element, label) {
320951
- if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
320952
- return true;
320953
- return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
320954
- const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
320955
- return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
320956
- });
320957
- }
320958
- function stableMobileControlIdentity(element) {
320959
- const role = normalizeControlIdentityPart(element.role || "control");
320960
- const accessibilityId = element.accessibilityId?.trim();
320961
- if (accessibilityId)
320962
- return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
320963
- const resourceId = element.resourceId?.trim();
320964
- if (resourceId)
320965
- return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
320966
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
320967
- if (label)
320968
- return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
320969
- return null;
320970
- }
320971
- function classifySafeMobileNavigationControl(element) {
320972
- if (!element.actionable || element.enabled === false)
320973
- return null;
320974
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
320975
- const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
320976
- if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
320977
- return null;
320978
- if (isSystemBackNavigationControl(element, label))
320979
- return null;
320980
- const role = element.role.trim();
320981
- if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
320982
- return null;
320983
- const linkLike = /(?:Link$|^a$)/i.test(role);
320984
- let kind = null;
320985
- if (/(?:TabBarItem|\btab\b)/i.test(role))
320986
- kind = "tab";
320987
- else if (/menuitem/i.test(role))
320988
- kind = "menu-item";
320989
- else if (linkLike && element.navigationProvenance)
320990
- kind = "link";
320991
- else if (linkLike)
320992
- return null;
320993
- else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
320994
- kind = "menu-trigger";
320995
- else if (element.expanded === false)
320996
- kind = "disclosure";
320997
- else if (MENU_TRIGGER_LABEL_RE.test(label))
320998
- kind = "menu-trigger";
320999
- else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
321000
- kind = "modal-trigger";
321001
- else if (element.navigationProvenance)
321002
- kind = "navigation-button";
321003
- else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
321004
- kind = "navigation-button";
321005
- if (!kind)
321006
- return null;
321007
- const identity = stableMobileControlIdentity(element);
321008
- return identity ? { identity, label, kind } : null;
321009
- }
321010
- function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
321011
- if (!element.navigationProvenance)
321012
- return void 0;
321013
- const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
321014
- if (!durableId && !semanticFamilyId)
321015
- return void 0;
321016
- return [
321017
- "shared-navigation",
321018
- element.navigationProvenance,
321019
- !durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
321020
- candidate.kind,
321021
- durableId || "semantic-only",
321022
- normalizeControlIdentityPart(candidate.label)
321023
- ].filter(Boolean).join(":");
321024
- }
321025
- function mobileNavigationRegion(element, windowSize) {
321026
- if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
321027
- return "unknown";
321028
- const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
321029
- const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
321030
- if (centerY >= 0.72)
321031
- return "bottom";
321032
- if (centerY <= 0.28)
321033
- return "top";
321034
- if (centerX <= 0.35)
321035
- return "left";
321036
- if (centerX >= 0.65)
321037
- return "right";
321038
- return "center";
321039
- }
321040
- function buildSemanticNavigationGroups(elements, windowSize) {
321041
- const groups = /* @__PURE__ */ new Map();
321042
- for (const element of elements) {
321043
- if (!element.navigationProvenance)
321044
- continue;
321045
- const candidate = classifySafeMobileNavigationControl(element);
321046
- if (!candidate)
321047
- continue;
321048
- const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
321049
- const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
321050
- labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
321051
- groups.set(groupKey, labels);
321052
- }
321053
- return new Map([...groups].filter(([, labels]) => labels.size >= 2));
321054
- }
321055
- function deriveScreenId(signal) {
321056
- const namespace = signal.activity?.trim() || signal.bundleId?.trim();
321057
- return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
321058
- }
321059
- function observedElementIdentity(element) {
321060
- return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
321061
- }
321062
- function mergeObservedElements(previous, current) {
321063
- const merged = [...current];
321064
- const seen = new Set(current.map(observedElementIdentity));
321065
- for (const element of previous) {
321066
- const identity = observedElementIdentity(element);
321067
- if (seen.has(identity))
321068
- continue;
321069
- seen.add(identity);
321070
- merged.push(element);
321071
- }
321072
- return merged;
321073
- }
321074
- function sanitizeElementsForEvidence(elements) {
321075
- return elements.map((element) => {
321076
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
321077
- return {
321078
- ...element,
321079
- ...label ? { label } : { label: void 0 },
321080
- text: void 0
321081
- };
321082
- });
321083
- }
321084
- var MobileExplorationState = class {
321085
- /** screenId -> ScreenState (the mobile analog of routeState). */
321086
- screens = /* @__PURE__ */ new Map();
321087
- /** Observed directed transitions between screens. */
321088
- transitions = [];
321089
- /** Transition-announced screens that do not yet have a live observation. */
321090
- frontier = /* @__PURE__ */ new Map();
321091
- /** Safe navigation controls observed on each screen, keyed by stable identity. */
321092
- safeControlsByScreen = /* @__PURE__ */ new Map();
321093
- /** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
321094
- attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
321095
- successfulSafeControlAttemptCount = 0;
321096
- /** Repeated id-less structural navigation families, grouped by region. */
321097
- semanticNavigationFamilies = /* @__PURE__ */ new Map();
321098
- nextSemanticNavigationFamilyId = 1;
321099
- /** The screen-id of the most recently observed screen. */
321100
- currentScreenId = null;
321101
- /** Latest iteration recorded (drives stale detection + firstSeen stamps). */
321102
- iteration = 0;
321103
- /** Stale detector: progress mark (#screens + #total elements) at last advance. */
321104
- lastProgressMark = 0;
321105
- /** Consecutive iterations observed with no progress. */
321106
- staleIterations = 0;
321107
- staleThreshold;
321108
- constructor(options = {}) {
321109
- this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
321110
- }
321111
- // -------------------------------------------------------------------------
321112
- // Observation recording
321113
- // -------------------------------------------------------------------------
321114
- /**
321115
- * Returns true when `signal` identifies a screen this run has never observed
321116
- * before. Pure read — does not mutate state.
321117
- */
321118
- isNewScreen(signal) {
321119
- return !this.screens.has(deriveScreenId(signal));
321120
- }
321121
- /**
321122
- * Record an observation of `screen` at `iteration`. Creates a `ScreenState`
321123
- * on first sight (stamping `firstSeenIteration`), otherwise refreshes the
321124
- * stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
321125
- * element refresh). A driver observation already contains the structural UI
321126
- * tree, elements, screenshot association, and screen identity, so it is
321127
- * captured evidence immediately; the model's capture_screen call only adds
321128
- * a friendly name/type. Returns the resolved screen-id.
321129
- */
321130
- recordObservation(signal, screen, iteration) {
321131
- this.iteration = iteration;
321132
- const screenId = deriveScreenId(signal);
321133
- const evidenceElements = sanitizeElementsForEvidence(screen.elements);
321134
- const existing = this.screens.get(screenId);
321135
- if (existing) {
321136
- this.screens.set(screenId, {
321137
- ...existing,
321138
- elements: mergeObservedElements(existing.elements, evidenceElements)
321139
- });
321140
- } else {
321141
- this.screens.set(screenId, {
321142
- screenId,
321143
- firstSeenIteration: iteration,
321144
- elements: evidenceElements,
321145
- visited: true
321146
- });
321147
- }
321148
- this.frontier.delete(screenId);
321149
- const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
321150
- const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
321151
- const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
321152
- for (const [groupKey, labels] of semanticNavigationGroups) {
321153
- semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
321154
- }
321155
- for (const element of evidenceElements) {
321156
- const candidate = classifySafeMobileNavigationControl(element);
321157
- if (!candidate)
321158
- continue;
321159
- const storedIdentity = storedMobileControlIdentity(candidate);
321160
- const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
321161
- controls.set(storedIdentity, {
321162
- ...candidate,
321163
- identity: storedIdentity,
321164
- sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
321165
- });
321166
- }
321167
- if (controls.size > 0)
321168
- this.safeControlsByScreen.set(screenId, controls);
321169
- this.currentScreenId = screenId;
321170
- this.updateStaleProgress();
321171
- return screenId;
321172
- }
321173
- /**
321174
- * Back-compatible acknowledgement from capture_screen. Observation already
321175
- * marks structural evidence visited; this call only resolves a rare pending
321176
- * transition frontier and preserves callers that annotate explicitly.
321177
- */
321178
- markVisited(screenId) {
321179
- const state2 = this.screens.get(screenId);
321180
- if (state2 && !state2.visited) {
321181
- this.screens.set(screenId, { ...state2, visited: true });
321182
- }
321183
- this.frontier.delete(screenId);
321184
- }
321185
- /**
321186
- * Record a confirmed tap against a safe navigation candidate. Callers invoke
321187
- * this only after the driver reports success; recovery-unknown, stale-ref, and
321188
- * failed actions must remain on the breadth frontier.
321189
- */
321190
- noteControlAttempt(screenId, element) {
321191
- if (!screenId)
321192
- return false;
321193
- const candidate = classifySafeMobileNavigationControl(element);
321194
- const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
321195
- if (candidate) {
321196
- const storedIdentity = storedMobileControlIdentity(candidate);
321197
- const existing = controls.get(storedIdentity);
321198
- controls.set(storedIdentity, {
321199
- ...candidate,
321200
- identity: storedIdentity,
321201
- sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
321202
- });
321203
- this.safeControlsByScreen.set(screenId, controls);
321204
- }
321205
- const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
321206
- if (!tappedLabel)
321207
- return false;
321208
- const before = this.attemptedSafeControlCoverageIdentities.size;
321209
- const alias = semanticControlAlias(tappedLabel);
321210
- for (const control of controls.values()) {
321211
- if (semanticControlAlias(control.label) === alias) {
321212
- this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
321213
- if (control.sharedCoverageIdentity) {
321214
- this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
321215
- }
321216
- }
321217
- }
321218
- if (this.attemptedSafeControlCoverageIdentities.size > before) {
321219
- this.successfulSafeControlAttemptCount += 1;
321220
- this.staleIterations = 0;
321221
- return true;
321222
- }
321223
- return false;
321224
- }
321225
- /** Attach a human-friendly name / coarse type to a screen (capture_screen). */
321226
- annotateScreen(screenId, annotation) {
321227
- const state2 = this.screens.get(screenId);
321228
- if (!state2)
321229
- return;
321230
- this.screens.set(screenId, {
321231
- ...state2,
321232
- screenName: annotation.screenName ?? state2.screenName,
321233
- screenType: annotation.screenType ?? state2.screenType
321234
- });
321235
- }
321236
- /** Attach structurally observed authentication provenance to a screen. */
321237
- setScreenAuthEvidence(screenId, evidence) {
321238
- const state2 = this.screens.get(screenId);
321239
- if (!state2)
321240
- return;
321241
- this.screens.set(screenId, {
321242
- ...state2,
321243
- requiresAuth: evidence.requiresAuth,
321244
- authProvenance: evidence.authProvenance,
321245
- sessionState: { ...evidence.sessionState }
321246
- });
321247
- }
321248
- /** Append a free-form observation note to a screen (observations tool). */
321249
- addScreenObservation(screenId, note) {
321250
- const state2 = this.screens.get(screenId);
321251
- if (!state2)
321252
- return;
321253
- const normalized = note.trim();
321254
- if (!normalized)
321255
- return;
321256
- const current = state2.observations ?? [];
321257
- if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
321258
- return;
321259
- }
321260
- const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
321261
- this.screens.set(screenId, { ...state2, observations });
321262
- }
321263
- /**
321264
- * Record a directed transition between two screens. Both endpoints are
321265
- * ensured to exist on the frontier if not yet observed (mirrors the web
321266
- * reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
321267
- * `toScreenId` becomes the current screen when the transition starts from the
321268
- * current screen.
321269
- */
321270
- recordTransition(transition) {
321271
- let recordedTransition = transition;
321272
- if (!transition.viaControlIdentity && transition.viaRef) {
321273
- const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
321274
- if (sourceElement) {
321275
- recordedTransition = {
321276
- ...transition,
321277
- viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
321278
- };
321279
- }
321280
- }
321281
- const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
321282
- if (!alreadyRecorded) {
321283
- this.transitions.push(recordedTransition);
321284
- }
321285
- for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
321286
- if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
321287
- this.frontier.set(endpointId, {
321288
- screenId: endpointId,
321289
- source: "transition",
321290
- discoveredAtIteration: this.iteration
321291
- });
321292
- }
321293
- }
321294
- if (transition.fromScreenId === this.currentScreenId) {
321295
- this.currentScreenId = transition.toScreenId;
321296
- }
321297
- }
321298
- // -------------------------------------------------------------------------
321299
- // Selectors
321300
- // -------------------------------------------------------------------------
321301
- /** The current screen-id (most recently observed), or null before any observation. */
321302
- getCurrentScreenId() {
321303
- return this.currentScreenId;
321304
- }
321305
- /** ScreenState for `screenId`, if known. */
321306
- getScreen(screenId) {
321307
- return this.screens.get(screenId);
321308
- }
321309
- /** Number of distinct screens observed this run. */
321310
- getScreenCount() {
321311
- return this.screens.size;
321312
- }
321313
- /** Number of screens backed by a live structural driver observation. */
321314
- getVisitedScreenCount() {
321315
- let total = 0;
321316
- for (const state2 of this.screens.values()) {
321317
- if (state2.visited)
321318
- total += 1;
321319
- }
321320
- return total;
321321
- }
321322
- /** Total elements observed across all known screens (latest snapshot each). */
321323
- getTotalElementCount() {
321324
- let total = 0;
321325
- for (const state2 of this.screens.values()) {
321326
- total += state2.elements.length;
321327
- }
321328
- return total;
321329
- }
321330
- /**
321331
- * Frontier screen-ids ranked by recency (most recently discovered first),
321332
- * excluding any screen already visited. Mirrors the web `getRankedFrontier`
321333
- * intent (filter visited, rank by recency) — without URL prefix similarity,
321334
- * which has no mobile analog.
321335
- */
321336
- getFrontier() {
321337
- return [...this.frontier.values()].filter((item) => {
321338
- const state2 = this.screens.get(item.screenId);
321339
- return !state2 || !state2.visited;
321340
- }).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
321341
- }
321342
- /** Safe navigation candidates not yet attempted, deduped across shared chrome. */
321343
- getUnexploredSafeNavigationCandidates() {
321344
- const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
321345
- const candidates = [];
321346
- for (const [screenId, controls] of this.safeControlsByScreen) {
321347
- const origin = this.screens.get(screenId);
321348
- const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
321349
- if (authenticatedProductObserved && originIsConfirmedPublicEntry)
321350
- continue;
321351
- for (const control of controls.values()) {
321352
- const alias = semanticControlAlias(control.label);
321353
- const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
321354
- if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
321355
- continue;
321356
- const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
321357
- if (existing) {
321358
- existing.screenIds.add(screenId);
321359
- } else {
321360
- candidates.push({
321361
- identity: coverageIdentity,
321362
- label: control.label,
321363
- kind: control.kind,
321364
- alias,
321365
- screenIds: /* @__PURE__ */ new Set([screenId])
321366
- });
321367
- }
321368
- }
321369
- }
321370
- return candidates.map((candidate) => ({
321371
- identity: candidate.identity,
321372
- label: candidate.label,
321373
- kind: candidate.kind,
321374
- screenIds: [...candidate.screenIds]
321375
- })).sort((a, b) => {
321376
- const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
321377
- const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
321378
- if (aCurrent !== bCurrent)
321379
- return aCurrent ? -1 : 1;
321380
- return b.screenIds.length - a.screenIds.length;
321381
- });
321382
- }
321383
- /** Harness-owned survey completion floor, independent of model assertions. */
321384
- getSurveyBreadthCoverage() {
321385
- const unvisitedScreenIds = this.getFrontier();
321386
- const unexploredControls = this.getUnexploredSafeNavigationCandidates();
321387
- const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
321388
- const missing = [];
321389
- if (unvisitedScreenIds.length > 0) {
321390
- missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
321391
- }
321392
- if (unexploredControls.length > 0) {
321393
- missing.push(`${unexploredControls.length} safe navigation control(s)`);
321394
- }
321395
- return {
321396
- required: true,
321397
- complete,
321398
- unvisitedScreenIds,
321399
- unexploredControls,
321400
- attemptedControlCount: this.successfulSafeControlAttemptCount,
321401
- ...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
321402
- };
321403
- }
321404
- /**
321405
- * Monotonic structural progress token for adaptive survey leases. Frontier
321406
- * cardinality alone is insufficient: attempting control A can reveal control
321407
- * B and leave the remaining count unchanged even though real work happened.
321408
- */
321409
- getSurveyBreadthProgressMark() {
321410
- const discoveredControlIdentities = /* @__PURE__ */ new Set();
321411
- for (const [screenId, controls] of this.safeControlsByScreen) {
321412
- for (const control of controls.values()) {
321413
- discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
321414
- }
321415
- }
321416
- return [
321417
- `screens:${this.screens.size}`,
321418
- `attempts:${this.successfulSafeControlAttemptCount}`,
321419
- `controls:${discoveredControlIdentities.size}`,
321420
- `transitions:${this.transitions.length}`
321421
- ].join("|");
321422
- }
321423
- resolveSemanticNavigationFamilyId(groupKey, labels) {
321424
- const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
321425
- let best;
321426
- let bestIntersection = 0;
321427
- for (const family of families) {
321428
- let intersection = 0;
321429
- for (const label of labels) {
321430
- if (family.labels.has(label))
321431
- intersection += 1;
321432
- }
321433
- const overlap = intersection / Math.min(labels.size, family.labels.size);
321434
- if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
321435
- best = family;
321436
- bestIntersection = intersection;
321437
- }
321438
- }
321439
- if (best) {
321440
- for (const label of labels)
321441
- best.labels.add(label);
321442
- return best.id;
321443
- }
321444
- const created = {
321445
- id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
321446
- labels: new Set(labels)
321447
- };
321448
- families.push(created);
321449
- this.semanticNavigationFamilies.set(groupKey, families);
321450
- return created.id;
321451
- }
321452
- /** Observed transitions (defensive copy). */
321453
- getTransitions() {
321454
- return [...this.transitions];
321455
- }
321456
- // -------------------------------------------------------------------------
321457
- // Stale-progress detection (ported semantics from the web stale guard)
321458
- // -------------------------------------------------------------------------
321459
- /**
321460
- * Progress mark: distinct screens weighted heavily, plus total elements seen.
321461
- * A run advances this mark whenever it finds a new screen OR new elements;
321462
- * the stale detector triggers when the mark stops advancing for
321463
- * `staleThreshold` consecutive iterations.
321464
- */
321465
- getProgressMark() {
321466
- return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
321467
- }
321468
- updateStaleProgress() {
321469
- const mark = this.getProgressMark();
321470
- if (mark > this.lastProgressMark) {
321471
- this.lastProgressMark = mark;
321472
- this.staleIterations = 0;
321473
- } else {
321474
- this.staleIterations += 1;
321475
- }
321476
- }
321477
- /** Consecutive no-progress iterations observed so far. */
321478
- getStaleIterations() {
321479
- return this.staleIterations;
321480
- }
321481
- /**
321482
- * True when the run has gone `staleThreshold` consecutive iterations with no
321483
- * new screen and no new element — the signal the explorer uses to nudge or
321484
- * finish (mirrors the web explorer's stale guard).
321485
- */
321486
- isStaleProgress() {
321487
- return this.staleIterations >= this.staleThreshold;
321488
- }
321489
- // -------------------------------------------------------------------------
321490
- // Planner evidence export
321491
- // -------------------------------------------------------------------------
321492
- /**
321493
- * Snapshot the screen-graph as a plain object (frozen mobile-types
321494
- * `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
321495
- * state.
321496
- */
321497
- toScreenGraph() {
321498
- const screens = /* @__PURE__ */ new Map();
321499
- for (const [id, state2] of this.screens) {
321500
- screens.set(id, {
321501
- ...state2,
321502
- elements: [...state2.elements],
321503
- observations: state2.observations ? [...state2.observations] : void 0,
321504
- sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
321505
- });
321506
- }
321507
- return {
321508
- screens,
321509
- transitions: this.transitions.map((t) => ({
321510
- fromScreenId: t.fromScreenId,
321511
- toScreenId: t.toScreenId,
321512
- viaRef: t.viaRef,
321513
- viaControlIdentity: t.viaControlIdentity,
321514
- action: t.action
321515
- }))
321516
- };
321517
- }
321518
- /**
321519
- * Build the planner evidence shape: one `CollectedPageData` per screen, with
321520
- * the screen-id carried in BOTH `route` (the on-the-wire positional field the
321521
- * server keys screenshots by) AND the explicit `screenId` field (per the
321522
- * Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
321523
- * the screen's parsed actionable elements into the platform-neutral
321524
- * `CollectedPageElement[]` shape the server persists as SiteElements — without
321525
- * this, mobile screens land with zero elements (coverage always 0%, no element
321526
- * intelligence). `triggersNavigation` is derived from the durable source
321527
- * element identity captured when the transition happened (with ref-only
321528
- * fallback for legacy transitions). Insertion order matches first-seen order.
321529
- */
321530
- buildInMemoryScreenMap() {
321531
- const pages = [];
321532
- const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
321533
- const navRefsByScreen = /* @__PURE__ */ new Map();
321534
- const navIdentitiesByScreen = /* @__PURE__ */ new Map();
321535
- for (const transition of this.transitions) {
321536
- if (transition.viaControlIdentity) {
321537
- const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
321538
- set.add(transition.viaControlIdentity);
321539
- navIdentitiesByScreen.set(transition.fromScreenId, set);
321540
- } else if (transition.viaRef) {
321541
- const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
321542
- set.add(transition.viaRef);
321543
- navRefsByScreen.set(transition.fromScreenId, set);
321544
- }
321545
- }
321546
- for (const state2 of ordered) {
321547
- const page = {
321548
- route: state2.screenId,
321549
- screenId: state2.screenId,
321550
- pageType: state2.screenType ?? "UNKNOWN",
321551
- elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
321552
- navigationRefs: navRefsByScreen.get(state2.screenId),
321553
- navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
321554
- }),
321555
- requiresAuth: state2.requiresAuth ?? false
321556
- };
321557
- if (state2.sessionState) {
321558
- page.sessionState = { ...state2.sessionState };
321559
- }
321560
- if (state2.screenName) {
321561
- page.title = state2.screenName;
321562
- }
321563
- if (state2.observations && state2.observations.length > 0) {
321564
- page.observations = state2.observations.map((note) => ({
321565
- action: "observe",
321566
- outcome: note,
321567
- outcomeType: "SUCCESS"
321568
- }));
321569
- }
321570
- pages.push(page);
321571
- }
321572
- return pages;
321573
- }
321574
- /** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
321575
- toScreenMap() {
321576
- return this.buildInMemoryScreenMap();
321577
- }
321578
- };
321579
- exports.MobileExplorationState = MobileExplorationState;
321580
- }
321581
- });
321582
-
321583
321741
  // ../runner-core/dist/services/mobile/mobile-discovery-agent.js
321584
321742
  var require_mobile_discovery_agent = __commonJS({
321585
321743
  "../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports) {
@@ -321875,7 +322033,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
321875
322033
  };
321876
322034
  }
321877
322035
  if (isSurvey && explore.authCoverage && !explore.authCoverage.complete) {
321878
- throw new Error("Mobile survey authentication coverage is incomplete; planning was skipped. " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
322036
+ log2("Mobile survey continuing to planning with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
321879
322037
  }
321880
322038
  if (isSurvey && explore.breadthCoverage && !explore.breadthCoverage.complete) {
321881
322039
  log2("Mobile survey continuing to planning with residual breadth gaps: " + (explore.breadthCoverage.reason ?? "Unvisited screens or safe navigation controls remain."));
@@ -322136,6 +322294,9 @@ Fatal error: ${msg}`);
322136
322294
  if (explore.breadthCoverage && !explore.breadthCoverage.complete) {
322137
322295
  warnings.push("Discovery retained the captured mobile screen graph with residual navigation gaps: " + (explore.breadthCoverage.reason ?? "some observed controls could not be revisited."));
322138
322296
  }
322297
+ if (explore.authCoverage && !explore.authCoverage.complete) {
322298
+ warnings.push("Discovery retained the captured mobile screen graph with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "no authenticated product screen was proven."));
322299
+ }
322139
322300
  if (explore.truncated) {
322140
322301
  const reasonLabel = explore.truncatedReason === "rate_limit" ? "AI rate-limit" : explore.truncatedReason === "timeout" ? "AI timeout" : "AI error";
322141
322302
  warnings.push(`Discovery may be incomplete: exploration aborted by ${reasonLabel}. Re-run after the limit clears for a complete screen graph.`);
@@ -336645,7 +336806,7 @@ var require_package4 = __commonJS({
336645
336806
  "package.json"(exports, module) {
336646
336807
  module.exports = {
336647
336808
  name: "@validate.qa/runner",
336648
- version: "1.0.38",
336809
+ version: "1.0.39",
336649
336810
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
336650
336811
  bin: {
336651
336812
  "validate-runner": "dist/cli.js"
@@ -339903,8 +340064,9 @@ async function activateTargetApp(sessionId, platform3, appId) {
339903
340064
  const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
339904
340065
  await executeMobileScript(sessionId, "mobile: activateApp", args);
339905
340066
  }
340067
+ var APP_TERMINATE_TIMEOUT_MS = 5e3;
339906
340068
  async function terminateTargetApp(sessionId, platform3, appId) {
339907
- const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
340069
+ const args = platform3 === "IOS" ? [{ bundleId: appId, timeout: APP_TERMINATE_TIMEOUT_MS }] : [{ appId, timeout: APP_TERMINATE_TIMEOUT_MS }];
339908
340070
  await executeMobileScript(sessionId, "mobile: terminateApp", args);
339909
340071
  }
339910
340072
  async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
@@ -342915,7 +343077,11 @@ var MobileBridgeDriver = class {
342915
343077
  async relaunch() {
342916
343078
  await this.withSessionRecovery("app relaunch", async () => {
342917
343079
  const args = this.appLifecycleArgs();
342918
- await this.executeScript("mobile: terminateApp", [args]);
343080
+ try {
343081
+ await this.executeScript("mobile: terminateApp", [{ ...args, timeout: 5e3 }]);
343082
+ } catch (err) {
343083
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
343084
+ }
342919
343085
  await this.executeScript("mobile: activateApp", [args]);
342920
343086
  try {
342921
343087
  await waitForColdBootSettle(this.sessionPath);