@validate.qa/runner 1.0.5 → 1.0.8

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 +496 -158
  2. package/dist/cli.mjs +496 -158
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1037,6 +1037,65 @@ var require_mobile_source_parser = __commonJS({
1037
1037
  }
1038
1038
  });
1039
1039
 
1040
+ // ../runner-core/dist/services/mobile/mobile-boundary.js
1041
+ var require_mobile_boundary = __commonJS({
1042
+ "../runner-core/dist/services/mobile/mobile-boundary.js"(exports2) {
1043
+ "use strict";
1044
+ Object.defineProperty(exports2, "__esModule", { value: true });
1045
+ exports2.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
1046
+ exports2.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
1047
+ exports2.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
1048
+ exports2.isTargetLikeMobileBoundary = isTargetLikeMobileBoundary2;
1049
+ exports2.mobileBoundaryNote = mobileBoundaryNote;
1050
+ var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
1051
+ "com.android.permissioncontroller",
1052
+ "com.google.android.permissioncontroller",
1053
+ "com.android.packageinstaller",
1054
+ "com.google.android.packageinstaller"
1055
+ ]);
1056
+ var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
1057
+ "com.apple.springboard"
1058
+ ]);
1059
+ function mobileOwnerFromScreenSignal(signal, targetAppId) {
1060
+ const bundleId = signal.bundleId?.trim();
1061
+ if (bundleId)
1062
+ return bundleId;
1063
+ const activity = signal.activity?.trim();
1064
+ if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
1065
+ return targetAppId;
1066
+ }
1067
+ return null;
1068
+ }
1069
+ function isTransientMobileSystemOwner(owner, platform3) {
1070
+ return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
1071
+ }
1072
+ function classifyMobileScreenBoundary2(signal, targetAppId, platform3) {
1073
+ if (!targetAppId)
1074
+ return { kind: "target" };
1075
+ const owner = mobileOwnerFromScreenSignal(signal, targetAppId);
1076
+ if (!owner)
1077
+ return { kind: "unknown" };
1078
+ if (owner === targetAppId)
1079
+ return { kind: "target" };
1080
+ if (isTransientMobileSystemOwner(owner, platform3)) {
1081
+ return { kind: "transient_external", owner, targetAppId };
1082
+ }
1083
+ return { kind: "external", owner, targetAppId };
1084
+ }
1085
+ function isTargetLikeMobileBoundary2(boundary) {
1086
+ return boundary.kind === "target" || boundary.kind === "unknown";
1087
+ }
1088
+ function mobileBoundaryNote(boundary) {
1089
+ return [
1090
+ "",
1091
+ "## App Boundary",
1092
+ `Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
1093
+ boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the target app evidence and relaunched the target app."
1094
+ ].join("\n");
1095
+ }
1096
+ }
1097
+ });
1098
+
1040
1099
  // ../shared/dist/types.js
1041
1100
  function isUIVariant(value) {
1042
1101
  return typeof value === "string" && UI_VARIANTS.includes(value);
@@ -1205,7 +1264,8 @@ var init_constants = __esm({
1205
1264
  "GROK_API_KEY",
1206
1265
  "MINIMAX_API_KEY",
1207
1266
  "ANTHROPIC_API_KEY",
1208
- "NVIDIA_API_KEY"
1267
+ "NVIDIA_API_KEY",
1268
+ "DEEPSEEK_API_KEY"
1209
1269
  ]);
1210
1270
  SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1211
1271
  // Runtime essentials - Node.js and Playwright need these to function
@@ -1472,6 +1532,18 @@ function buildAppiumCode(testName, target, steps) {
1472
1532
  ` }`,
1473
1533
  ` throw new Error('No locator resolved. Tried: ' + JSON.stringify(selectors) + (__lastError ? ' (last error: ' + String(__lastError) + ')' : ''));`,
1474
1534
  `}`,
1535
+ ``,
1536
+ `async function __tapAt(driver: WebdriverIO.Browser, x: number, y: number) {`,
1537
+ ` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x, y }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`,
1538
+ `}`,
1539
+ ``,
1540
+ `async function __typeIntoFocused(driver: WebdriverIO.Browser, text: string) {`,
1541
+ ` try {`,
1542
+ ` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(text);`,
1543
+ ` } catch {`,
1544
+ ` await driver.keys(text);`,
1545
+ ` }`,
1546
+ `}`,
1475
1547
  ...hasSwipe ? [
1476
1548
  ``,
1477
1549
  `// Read the device window with a few retries (matches the runtime executor).`,
@@ -1530,13 +1602,25 @@ function buildAppiumCode(testName, target, steps) {
1530
1602
  const y = Math.round(bounds.y + bounds.height / 2);
1531
1603
  lines.push(` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x: ${x}, y: ${y} }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`);
1532
1604
  } else if (step.action === "type" && hasSelector) {
1533
- lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
1605
+ if (bounds) {
1606
+ const x = Math.round(bounds.x + bounds.width / 2);
1607
+ const y = Math.round(bounds.y + bounds.height / 2);
1608
+ lines.push(` try {`);
1609
+ lines.push(` await __tapAt(driver, ${x}, ${y});`);
1610
+ lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
1611
+ lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
1612
+ lines.push(` } catch {`);
1613
+ lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
1614
+ lines.push(` }`);
1615
+ } else {
1616
+ lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
1617
+ }
1534
1618
  } else if (step.action === "type" && bounds) {
1535
1619
  const x = Math.round(bounds.x + bounds.width / 2);
1536
1620
  const y = Math.round(bounds.y + bounds.height / 2);
1537
- lines.push(` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x: ${x}, y: ${y} }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`);
1621
+ lines.push(` await __tapAt(driver, ${x}, ${y});`);
1538
1622
  lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
1539
- lines.push(` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(${toJsStringLiteral(step.value ?? "")});`);
1623
+ lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
1540
1624
  } else if (step.action === "clear" && hasSelector) {
1541
1625
  lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
1542
1626
  } else if (step.action === "assertVisible" && hasSelector) {
@@ -2050,6 +2134,7 @@ var init_audit_pricing = __esm({
2050
2134
  "MiniMax-M2.7": { input: 0.3, output: 1.2 },
2051
2135
  "MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
2052
2136
  "MiniMax-M2": { input: 0.3, output: 1.2 },
2137
+ "deepseek-v4-flash": { input: 0.14, output: 0.28 },
2053
2138
  // Whisper bills per-minute, not per-token
2054
2139
  "whisper-1": null,
2055
2140
  // Historical / deprecated (kept so old audit rows still show a cost).
@@ -4433,21 +4518,13 @@ var require_mobile_explorer = __commonJS({
4433
4518
  var ai_retry_js_1 = require_ai_retry();
4434
4519
  var mobile_source_parser_js_1 = require_mobile_source_parser();
4435
4520
  var mobile_observation_js_1 = require_mobile_observation();
4521
+ var mobile_boundary_js_1 = require_mobile_boundary();
4436
4522
  var MAX_MOBILE_ITERATIONS_SURVEY = 40;
4437
4523
  var MAX_MOBILE_ITERATIONS_DEEP = 60;
4438
4524
  var MAX_SCREENSHOTS = 15;
4439
4525
  var MAX_RECENT_EXCHANGES = 24;
4440
4526
  var OBSERVATION_MAX_ELEMENTS = 50;
4441
4527
  var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
4442
- var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
4443
- "com.android.permissioncontroller",
4444
- "com.google.android.permissioncontroller",
4445
- "com.android.packageinstaller",
4446
- "com.google.android.packageinstaller"
4447
- ]);
4448
- var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
4449
- "com.apple.springboard"
4450
- ]);
4451
4528
  var MOBILE_SNAPSHOT_TOOL = {
4452
4529
  type: "function",
4453
4530
  function: {
@@ -4670,40 +4747,6 @@ var require_mobile_explorer = __commonJS({
4670
4747
  return "timeout";
4671
4748
  return "ai_error";
4672
4749
  }
4673
- function ownerFromScreenSignal(signal, targetAppId) {
4674
- const bundleId = signal.bundleId?.trim();
4675
- if (bundleId)
4676
- return bundleId;
4677
- const activity = signal.activity?.trim();
4678
- if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
4679
- return targetAppId;
4680
- }
4681
- return null;
4682
- }
4683
- function isTransientSystemOwner(owner, platform3) {
4684
- return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
4685
- }
4686
- function classifyMobileScreenBoundary(signal, targetAppId, platform3) {
4687
- if (!targetAppId)
4688
- return { kind: "target" };
4689
- const owner = ownerFromScreenSignal(signal, targetAppId);
4690
- if (!owner)
4691
- return { kind: "unknown" };
4692
- if (owner === targetAppId)
4693
- return { kind: "target" };
4694
- if (isTransientSystemOwner(owner, platform3)) {
4695
- return { kind: "transient_external", owner, targetAppId };
4696
- }
4697
- return { kind: "external", owner, targetAppId };
4698
- }
4699
- function boundaryNote(boundary) {
4700
- return [
4701
- "",
4702
- "## App Boundary",
4703
- `Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
4704
- boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the survey evidence."
4705
- ].join("\n");
4706
- }
4707
4750
  function resolveSnapshot(xml, windowSize, driverSignal, platform3) {
4708
4751
  const parsed = (0, mobile_source_parser_js_1.parseMobileSource)(xml, platform3);
4709
4752
  const signal = {
@@ -4901,10 +4944,10 @@ Use this to prioritize which screens and flows to cover.` : "";
4901
4944
  };
4902
4945
  const currentBoundary = () => {
4903
4946
  const current = getLatest();
4904
- return current ? classifyMobileScreenBoundary(current.signal, targetAppId, platform3) : { kind: "unknown" };
4947
+ return current ? (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(current.signal, targetAppId, platform3) : { kind: "unknown" };
4905
4948
  };
4906
4949
  const absorbResolvedObservation = async (resolved, screenshot, iterationForObservation, source) => {
4907
- const boundary = classifyMobileScreenBoundary(resolved.signal, targetAppId, platform3);
4950
+ const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
4908
4951
  if (boundary.kind === "target" || boundary.kind === "unknown") {
4909
4952
  const screenId = ingestSnapshot(resolved, iterationForObservation);
4910
4953
  captureScreenshot(screenshot, screenId);
@@ -4915,7 +4958,7 @@ Use this to prioritize which screens and flows to cover.` : "";
4915
4958
  if (boundary.kind === "transient_external") {
4916
4959
  log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
4917
4960
  return {
4918
- observation: `${resolved.observation}${boundaryNote(boundary)}`,
4961
+ observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
4919
4962
  screenId: null,
4920
4963
  transientExternal: true,
4921
4964
  externalOwner: boundary.owner
@@ -4934,7 +4977,7 @@ Use this to prioritize which screens and flows to cover.` : "";
4934
4977
  externalOwner: boundary.owner
4935
4978
  };
4936
4979
  }
4937
- const recoveredBoundary = classifyMobileScreenBoundary(recovered.signal, targetAppId, platform3);
4980
+ const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
4938
4981
  latest = recovered;
4939
4982
  lastSnapshotIteration = iterationForObservation;
4940
4983
  if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
@@ -4950,7 +4993,7 @@ Use this to prioritize which screens and flows to cover.` : "";
4950
4993
  if (recoveredBoundary.kind === "transient_external") {
4951
4994
  log2(` Relaunch surfaced system overlay ${recoveredBoundary.owner}; not adding it to target-app survey evidence.`);
4952
4995
  return {
4953
- observation: `${recovered.observation}${boundaryNote(recoveredBoundary)}`,
4996
+ observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
4954
4997
  screenId: null,
4955
4998
  externalBlocked: true,
4956
4999
  transientExternal: true,
@@ -8594,6 +8637,7 @@ var require_mobile_generator = __commonJS({
8594
8637
  var mobile_source_parser_js_1 = require_mobile_source_parser();
8595
8638
  var mobile_source_parser_js_2 = require_mobile_source_parser();
8596
8639
  var mobile_observation_js_1 = require_mobile_observation();
8640
+ var mobile_boundary_js_1 = require_mobile_boundary();
8597
8641
  var MAX_SCENARIO_ITERATIONS = 30;
8598
8642
  var MAX_RECENT_EXCHANGES = 24;
8599
8643
  var OBSERVATION_MAX_ELEMENTS = 50;
@@ -8779,8 +8823,9 @@ var require_mobile_generator = __commonJS({
8779
8823
  function hasStableId(element) {
8780
8824
  return !!nonEmpty2(element.accessibilityId) || !!nonEmpty2(element.resourceId);
8781
8825
  }
8782
- function buildSystemPrompt(platform3) {
8826
+ function buildSystemPrompt(platform3, targetAppId) {
8783
8827
  const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
8828
+ const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
8784
8829
  return `You are an expert mobile QA agent generating ONE automated test by driving a real ${platformLabel} app on a connected device. The app has just been relaunched into a clean state.
8785
8830
 
8786
8831
  ## How the loop works
@@ -8800,7 +8845,8 @@ var require_mobile_generator = __commonJS({
8800
8845
  - Re-snapshot before every assertion so the ref is fresh.
8801
8846
  - Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
8802
8847
  - Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
8803
- - Stay inside this app. Finish promptly once the outcome is proven.
8848
+ - 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.
8849
+ - Finish promptly once the outcome is proven.
8804
8850
 
8805
8851
  Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
8806
8852
  }
@@ -8873,7 +8919,7 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
8873
8919
  }
8874
8920
  }
8875
8921
  async function runScenarioLoop(args) {
8876
- const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, log: log2 } = args;
8922
+ const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, log: log2 } = args;
8877
8923
  const actions = [];
8878
8924
  let assertCount = 0;
8879
8925
  let finish = null;
@@ -8887,14 +8933,98 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
8887
8933
  const fromActivity = signal.activity ? signal.activity.split(".").pop() : void 0;
8888
8934
  return nonEmpty2(fromActivity) ?? nonEmpty2(signal.bundleId) ?? `screen-${signal.screenHash.slice(0, 8)}`;
8889
8935
  };
8890
- const ingest = (xml, driverSignal, windowSize) => {
8891
- if (!xml)
8892
- return null;
8893
- const resolved = resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
8936
+ const setLatest = (resolved) => {
8894
8937
  latest = resolved;
8895
8938
  lastScreenName = screenNameFor(resolved);
8896
8939
  return resolved;
8897
8940
  };
8941
+ const buildResolved = (xml, driverSignal, windowSize) => {
8942
+ if (!xml)
8943
+ return null;
8944
+ return resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
8945
+ };
8946
+ const emitScreenshot = (base64) => {
8947
+ if (!base64)
8948
+ return;
8949
+ try {
8950
+ onScreenshot?.(base64);
8951
+ } catch {
8952
+ }
8953
+ };
8954
+ const absorbResolvedObservation = async (resolved, screenshot, source) => {
8955
+ const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
8956
+ if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(boundary)) {
8957
+ setLatest(resolved);
8958
+ emitScreenshot(screenshot);
8959
+ return { resolved, observation: resolved.observation };
8960
+ }
8961
+ setLatest(resolved);
8962
+ if (boundary.kind === "transient_external") {
8963
+ log2(` system overlay detected from ${source}: ${boundary.owner}; not recording it as a target-app step.`);
8964
+ return {
8965
+ resolved,
8966
+ observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
8967
+ transientExternal: true,
8968
+ externalOwner: boundary.owner
8969
+ };
8970
+ }
8971
+ log2(` external app detected from ${source}: ${boundary.owner}; rejecting the action and relaunching ${boundary.targetAppId}.`);
8972
+ try {
8973
+ await driver.relaunch();
8974
+ const snap = await driver.snapshot();
8975
+ const recovered = buildResolved(snap.xml, snap.screenSignal, snap.windowSize);
8976
+ if (!recovered) {
8977
+ return {
8978
+ resolved: null,
8979
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunched target app, but no UI tree was returned)`,
8980
+ externalBlocked: true,
8981
+ externalOwner: boundary.owner
8982
+ };
8983
+ }
8984
+ const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
8985
+ setLatest(recovered);
8986
+ if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(recoveredBoundary)) {
8987
+ emitScreenshot(snap.screenshot);
8988
+ return {
8989
+ resolved: recovered,
8990
+ observation: recovered.observation,
8991
+ externalBlocked: true,
8992
+ externalOwner: boundary.owner
8993
+ };
8994
+ }
8995
+ if (recoveredBoundary.kind === "transient_external") {
8996
+ log2(` relaunch surfaced system overlay ${recoveredBoundary.owner}; not recording it as a target-app step.`);
8997
+ return {
8998
+ resolved: recovered,
8999
+ observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
9000
+ externalBlocked: true,
9001
+ transientExternal: true,
9002
+ externalOwner: boundary.owner
9003
+ };
9004
+ }
9005
+ log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
9006
+ return {
9007
+ resolved: recovered,
9008
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
9009
+ externalBlocked: true,
9010
+ externalOwner: boundary.owner
9011
+ };
9012
+ } catch (err) {
9013
+ const message = err instanceof Error ? err.message : String(err);
9014
+ return {
9015
+ resolved: null,
9016
+ observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; failed to relaunch target app: ${message})`,
9017
+ externalBlocked: true,
9018
+ externalOwner: boundary.owner
9019
+ };
9020
+ }
9021
+ };
9022
+ const absorbObservation = async (xml, driverSignal, windowSize, screenshot, source) => {
9023
+ const resolved = buildResolved(xml, driverSignal, windowSize);
9024
+ if (!resolved)
9025
+ return { resolved: null, observation: null };
9026
+ return absorbResolvedObservation(resolved, screenshot, source);
9027
+ };
8898
9028
  const resolveRef = (ref) => {
8899
9029
  if (typeof ref !== "string" || !latest)
8900
9030
  return null;
@@ -8902,11 +9032,11 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
8902
9032
  };
8903
9033
  try {
8904
9034
  const seed = await driver.snapshot();
8905
- ingest(seed.xml, seed.screenSignal, seed.windowSize);
9035
+ await absorbObservation(seed.xml, seed.screenSignal, seed.windowSize, seed.screenshot, "seed snapshot");
8906
9036
  } catch (err) {
8907
9037
  log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
8908
9038
  }
8909
- const systemPrompt = buildSystemPrompt(platform3);
9039
+ const systemPrompt = buildSystemPrompt(platform3, targetAppId);
8910
9040
  const kickoff = buildScenarioKickoff(scenario);
8911
9041
  const recentExchanges = [];
8912
9042
  let iteration = 0;
@@ -9017,7 +9147,7 @@ ${statePacket}` },
9017
9147
  driver,
9018
9148
  platform: platform3,
9019
9149
  latest: getLatest,
9020
- ingest,
9150
+ absorbObservation,
9021
9151
  resolveRef,
9022
9152
  screenName: () => lastScreenName || "UnknownScreen",
9023
9153
  recordAction: (action) => {
@@ -9040,60 +9170,85 @@ ${statePacket}` },
9040
9170
  return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
9041
9171
  }
9042
9172
  async function dispatchGenerateTool(args) {
9043
- const { toolName, toolArgs, driver, platform: platform3, latest, ingest, resolveRef, screenName, recordAction, log: log2 } = args;
9173
+ const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, screenName, recordAction, log: log2 } = args;
9044
9174
  const unknownRef = (ref) => ({
9045
9175
  ok: false,
9046
9176
  error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
9047
9177
  });
9178
+ const actionPayload = (result, absorbed) => {
9179
+ const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
9180
+ return {
9181
+ ok: result.ok && !blocked,
9182
+ ...result.error ? { error: result.error } : {},
9183
+ ...blocked ? { error: "Action left the target app and was not recorded. Continue from the relaunched target app." } : {},
9184
+ observation: absorbed.observation ?? void 0,
9185
+ ...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
9186
+ ...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
9187
+ };
9188
+ };
9189
+ const shouldRecordAction = (result, absorbed) => result.ok && !absorbed.externalBlocked && !absorbed.transientExternal;
9048
9190
  switch (toolName) {
9049
9191
  case "mobile_snapshot": {
9050
9192
  const snap = await driver.snapshot();
9051
- const resolved = ingest(snap.xml, snap.screenSignal, snap.windowSize);
9052
- if (!resolved)
9193
+ const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_snapshot");
9194
+ if (!absorbed.resolved)
9053
9195
  return { ok: false, error: "snapshot returned no UI tree" };
9054
- log2(` snapshot: ${resolved.screen.elements.length} elements`);
9055
- return { observation: resolved.observation };
9196
+ log2(` snapshot: ${absorbed.resolved.screen.elements.length} elements`);
9197
+ return {
9198
+ observation: absorbed.observation,
9199
+ ...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
9200
+ ...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
9201
+ };
9056
9202
  }
9057
9203
  case "mobile_tap": {
9058
9204
  const element = resolveRef(toolArgs.ref);
9059
9205
  if (!element)
9060
9206
  return unknownRef(toolArgs.ref);
9207
+ const beforeScreenName = screenName();
9061
9208
  const result = await driver.tap(element.bounds);
9062
- recordAction({
9063
- type: "TAP",
9064
- selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9065
- metadata: metadataForElement(element, screenName(), platform3)
9066
- });
9067
- const resolved = ingest(result.source, void 0, latest()?.screen.windowSize ?? null);
9068
- return { ok: result.ok, ...result.error ? { error: result.error } : {}, observation: resolved?.observation };
9209
+ const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
9210
+ if (shouldRecordAction(result, absorbed)) {
9211
+ recordAction({
9212
+ type: "TAP",
9213
+ selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9214
+ metadata: metadataForElement(element, beforeScreenName, platform3)
9215
+ });
9216
+ }
9217
+ return actionPayload(result, absorbed);
9069
9218
  }
9070
9219
  case "mobile_type": {
9071
9220
  const element = resolveRef(toolArgs.ref);
9072
9221
  if (!element)
9073
9222
  return unknownRef(toolArgs.ref);
9074
9223
  const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
9224
+ const beforeScreenName = screenName();
9075
9225
  const result = await driver.type(value, element.bounds);
9076
- recordAction({
9077
- type: "TYPE",
9078
- value,
9079
- selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9080
- metadata: metadataForElement(element, screenName(), platform3)
9081
- });
9082
- const resolved = ingest(result.source, void 0, latest()?.screen.windowSize ?? null);
9083
- return { ok: result.ok, ...result.error ? { error: result.error } : {}, observation: resolved?.observation };
9226
+ const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_type");
9227
+ if (shouldRecordAction(result, absorbed)) {
9228
+ recordAction({
9229
+ type: "TYPE",
9230
+ value,
9231
+ selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9232
+ metadata: metadataForElement(element, beforeScreenName, platform3)
9233
+ });
9234
+ }
9235
+ return actionPayload(result, absorbed);
9084
9236
  }
9085
9237
  case "mobile_clear": {
9086
9238
  const element = resolveRef(toolArgs.ref);
9087
9239
  if (!element)
9088
9240
  return unknownRef(toolArgs.ref);
9241
+ const beforeScreenName = screenName();
9089
9242
  const result = await driver.clear(element.bounds);
9090
- recordAction({
9091
- type: "CLEAR",
9092
- selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9093
- metadata: metadataForElement(element, screenName(), platform3)
9094
- });
9095
- const resolved = ingest(result.source, void 0, latest()?.screen.windowSize ?? null);
9096
- return { ok: result.ok, ...result.error ? { error: result.error } : {}, observation: resolved?.observation };
9243
+ const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_clear");
9244
+ if (shouldRecordAction(result, absorbed)) {
9245
+ recordAction({
9246
+ type: "CLEAR",
9247
+ selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
9248
+ metadata: metadataForElement(element, beforeScreenName, platform3)
9249
+ });
9250
+ }
9251
+ return actionPayload(result, absorbed);
9097
9252
  }
9098
9253
  case "mobile_swipe": {
9099
9254
  const direction = toolArgs.direction;
@@ -9101,30 +9256,42 @@ ${statePacket}` },
9101
9256
  return { ok: false, error: "direction must be one of up|down|left|right" };
9102
9257
  }
9103
9258
  const distance = typeof toolArgs.distance === "number" ? toolArgs.distance : void 0;
9259
+ const beforeScreenName = screenName();
9104
9260
  const result = await driver.swipe(direction, distance);
9105
- recordAction({
9106
- type: "SWIPE",
9107
- metadata: {
9108
- screenName: screenName(),
9109
- direction,
9110
- ...distance !== void 0 ? { distance } : {}
9111
- }
9112
- });
9113
- const resolved = ingest(result.source, void 0, latest()?.screen.windowSize ?? null);
9114
- return { ok: result.ok, ...result.error ? { error: result.error } : {}, observation: resolved?.observation };
9261
+ const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_swipe");
9262
+ if (shouldRecordAction(result, absorbed)) {
9263
+ recordAction({
9264
+ type: "SWIPE",
9265
+ metadata: {
9266
+ screenName: beforeScreenName,
9267
+ direction,
9268
+ ...distance !== void 0 ? { distance } : {}
9269
+ }
9270
+ });
9271
+ }
9272
+ return actionPayload(result, absorbed);
9115
9273
  }
9116
9274
  case "mobile_back": {
9275
+ const beforeScreenName = screenName();
9117
9276
  const result = await driver.back();
9118
- recordAction({ type: "BACK", metadata: { screenName: screenName() } });
9119
- const resolved = ingest(result.source, void 0, latest()?.screen.windowSize ?? null);
9120
- return { ok: result.ok, ...result.error ? { error: result.error } : {}, observation: resolved?.observation };
9277
+ const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_back");
9278
+ if (shouldRecordAction(result, absorbed)) {
9279
+ recordAction({ type: "BACK", metadata: { screenName: beforeScreenName } });
9280
+ }
9281
+ return actionPayload(result, absorbed);
9121
9282
  }
9122
9283
  case "mobile_relaunch": {
9123
9284
  await driver.relaunch();
9124
9285
  const snap = await driver.snapshot();
9125
- const resolved = ingest(snap.xml, snap.screenSignal, snap.windowSize);
9286
+ const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
9126
9287
  log2(" relaunched app for hermetic reset.");
9127
- return { ok: true, observation: resolved?.observation, note: "App relaunched (clean state). Not recorded as a step." };
9288
+ return {
9289
+ ok: true,
9290
+ observation: absorbed.observation,
9291
+ ...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
9292
+ ...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {},
9293
+ note: "App relaunched (clean state). Not recorded as a step."
9294
+ };
9128
9295
  }
9129
9296
  case "mobile_assert_visible": {
9130
9297
  const recorded = recordAssertVisible(toolArgs, { resolveRef, screenName, platform: platform3, recordAction });
@@ -9262,7 +9429,7 @@ ${statePacket}` },
9262
9429
  return test;
9263
9430
  }
9264
9431
  async function runMobileGeneratePhase(args) {
9265
- const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated } = args;
9432
+ const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
9266
9433
  const log2 = (line) => {
9267
9434
  try {
9268
9435
  onLog?.(line);
@@ -9276,6 +9443,7 @@ ${statePacket}` },
9276
9443
  const provider = (0, ai_provider_js_1.getProviderForModel)(agentModel);
9277
9444
  const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
9278
9445
  const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
9446
+ const targetAppId = ctx.target?.appId?.trim() || void 0;
9279
9447
  log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
9280
9448
  const tests = [];
9281
9449
  for (let i = 0; i < scenarios.length; i++) {
@@ -9294,6 +9462,8 @@ ${statePacket}` },
9294
9462
  aiClient,
9295
9463
  auditGroupId,
9296
9464
  onAICall,
9465
+ onScreenshot,
9466
+ targetAppId,
9297
9467
  log: log2
9298
9468
  });
9299
9469
  const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
@@ -311423,7 +311593,9 @@ var require_mobile_discovery_agent = __commonJS({
311423
311593
  return {
311424
311594
  collectedPages: explore.collectedPages,
311425
311595
  collectedTransitions,
311426
- pageScreenshots: explore.pageScreenshots,
311596
+ // Native screenshots are multi-megabyte PNGs. The live mirror streams them
311597
+ // through heartbeat telemetry; discovery checkpoints keep the screen graph
311598
+ // and transitions so large Android surveys cannot exceed server body limits.
311427
311599
  exploreStats: explore.exploreStats,
311428
311600
  healthObservations: explore.healthObservations
311429
311601
  };
@@ -311581,6 +311753,7 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
311581
311753
  ctx,
311582
311754
  onLog: (line) => log2(line),
311583
311755
  onAICall,
311756
+ onScreenshot,
311584
311757
  onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
311585
311758
  });
311586
311759
  const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
@@ -325286,7 +325459,7 @@ var require_live_browser_registry = __commonJS({
325286
325459
  var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
325287
325460
  var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
325288
325461
  var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
325289
- var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "2000000", 10) || 2e6;
325462
+ var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
325290
325463
  var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
325291
325464
  var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
325292
325465
  var LiveBrowserRegistry = class {
@@ -325782,6 +325955,7 @@ var require_dist2 = __commonJS({
325782
325955
  __exportStar(require_ai_provider(), exports2);
325783
325956
  __exportStar(require_mobile_types(), exports2);
325784
325957
  __exportStar(require_mobile_source_parser(), exports2);
325958
+ __exportStar(require_mobile_boundary(), exports2);
325785
325959
  var mobile_explorer_js_1 = require_mobile_explorer();
325786
325960
  Object.defineProperty(exports2, "runMobileExplorePhase", { enumerable: true, get: function() {
325787
325961
  return mobile_explorer_js_1.runMobileExplorePhase;
@@ -326114,7 +326288,7 @@ var require_package4 = __commonJS({
326114
326288
  "package.json"(exports2, module2) {
326115
326289
  module2.exports = {
326116
326290
  name: "@validate.qa/runner",
326117
- version: "1.0.5",
326291
+ version: "1.0.8",
326118
326292
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
326119
326293
  bin: {
326120
326294
  "validate-runner": "dist/cli.js"
@@ -326212,7 +326386,7 @@ var import_runner_core = __toESM(require_dist2());
326212
326386
  var import_runner_core2 = __toESM(require_dist2());
326213
326387
 
326214
326388
  // src/runner.ts
326215
- var import_runner_core9 = __toESM(require_dist2());
326389
+ var import_runner_core10 = __toESM(require_dist2());
326216
326390
 
326217
326391
  // src/services/regression-runner.ts
326218
326392
  var import_runner_core3 = __toESM(require_dist2());
@@ -326227,7 +326401,7 @@ var import_runner_core5 = __toESM(require_dist2());
326227
326401
  var import_runner_core6 = __toESM(require_dist2());
326228
326402
 
326229
326403
  // src/runner.ts
326230
- var import_runner_core10 = __toESM(require_dist2());
326404
+ var import_runner_core11 = __toESM(require_dist2());
326231
326405
 
326232
326406
  // src/services/auth-state.ts
326233
326407
  var INLINE_LOGIN_TAG = "@inline-login";
@@ -326247,6 +326421,7 @@ function shouldUseAuthState(tags, playwrightCode) {
326247
326421
 
326248
326422
  // src/services/appium-executor.ts
326249
326423
  init_dist();
326424
+ var import_runner_core8 = __toESM(require_dist2());
326250
326425
 
326251
326426
  // src/services/appium-lifecycle.ts
326252
326427
  var import_child_process = require("child_process");
@@ -327535,6 +327710,86 @@ async function openDeepLink(sessionId, platform3, deeplink, appId) {
327535
327710
  body: JSON.stringify({ script: "mobile: deepLink", args })
327536
327711
  });
327537
327712
  }
327713
+ function readExecuteStringValue(result) {
327714
+ if (result && typeof result === "object" && "value" in result) {
327715
+ const value = result.value;
327716
+ if (typeof value === "string" && value.trim()) return value.trim();
327717
+ }
327718
+ return void 0;
327719
+ }
327720
+ function readActiveBundleId(result) {
327721
+ if (result && typeof result === "object" && "value" in result) {
327722
+ const value = result.value;
327723
+ if (value && typeof value === "object" && "bundleId" in value) {
327724
+ const bundleId = value.bundleId;
327725
+ if (typeof bundleId === "string" && bundleId.trim()) return bundleId.trim();
327726
+ }
327727
+ }
327728
+ return void 0;
327729
+ }
327730
+ function normalizeAndroidActivity(activity, pkg2) {
327731
+ const trimmed = activity?.trim();
327732
+ if (!trimmed) return void 0;
327733
+ const packageName = pkg2?.trim();
327734
+ if (!packageName) return trimmed;
327735
+ if (trimmed.startsWith(".")) return `${packageName}${trimmed}`;
327736
+ if (!trimmed.includes(".")) return `${packageName}.${trimmed}`;
327737
+ return trimmed;
327738
+ }
327739
+ async function executeMobileScript(sessionId, script, args) {
327740
+ return wdRequest(`/session/${sessionId}/execute/sync`, {
327741
+ method: "POST",
327742
+ body: JSON.stringify({ script, args })
327743
+ });
327744
+ }
327745
+ async function readForegroundScreenSignal(sessionId, platform3) {
327746
+ if (platform3 === "ANDROID") {
327747
+ const [packageResult, activityResult] = await Promise.all([
327748
+ executeMobileScript(sessionId, "mobile: getCurrentPackage", []),
327749
+ executeMobileScript(sessionId, "mobile: getCurrentActivity", []).catch(() => void 0)
327750
+ ]);
327751
+ const pkg2 = readExecuteStringValue(packageResult);
327752
+ if (!pkg2) return null;
327753
+ const activity = normalizeAndroidActivity(readExecuteStringValue(activityResult), pkg2);
327754
+ return {
327755
+ platform: platform3,
327756
+ bundleId: pkg2,
327757
+ ...activity ? { activity } : {},
327758
+ screenHash: "foreground"
327759
+ };
327760
+ }
327761
+ const infoResult = await executeMobileScript(sessionId, "mobile: activeAppInfo", []);
327762
+ const bundleId = readActiveBundleId(infoResult);
327763
+ if (!bundleId) return null;
327764
+ return { platform: platform3, bundleId, screenHash: "foreground" };
327765
+ }
327766
+ async function activateTargetApp(sessionId, platform3, appId) {
327767
+ const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
327768
+ await executeMobileScript(sessionId, "mobile: activateApp", args);
327769
+ }
327770
+ async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
327771
+ let boundary = null;
327772
+ try {
327773
+ const signal = await readForegroundScreenSignal(sessionId, target.platform);
327774
+ boundary = signal ? (0, import_runner_core8.classifyMobileScreenBoundary)(signal, target.appId, target.platform) : null;
327775
+ } catch (err) {
327776
+ log2(` App boundary check skipped after ${source}: ${err instanceof Error ? err.message : String(err)}`);
327777
+ return;
327778
+ }
327779
+ if (!boundary || (0, import_runner_core8.isTargetLikeMobileBoundary)(boundary) || boundary.kind === "transient_external") {
327780
+ return;
327781
+ }
327782
+ const message = `Foreground app changed to ${boundary.owner}, outside target app ${boundary.targetAppId}`;
327783
+ log2(` ${message}; activating target app.`);
327784
+ try {
327785
+ await activateTargetApp(sessionId, target.platform, target.appId);
327786
+ } catch (err) {
327787
+ log2(` Could not reactivate ${target.appId}: ${err instanceof Error ? err.message : String(err)}`);
327788
+ }
327789
+ if (failOnExternal) {
327790
+ throw new Error(`${message}; relaunched target app and failed the step so mobile tests do not continue in another app.`);
327791
+ }
327792
+ }
327538
327793
  async function deleteSession(sessionId) {
327539
327794
  try {
327540
327795
  await wdRequest(`/session/${sessionId}`, { method: "DELETE" });
@@ -327661,6 +327916,41 @@ async function getActiveElementId(sessionId) {
327661
327916
  return null;
327662
327917
  }
327663
327918
  }
327919
+ async function sendKeysToFocusedInput(sessionId, value) {
327920
+ await wdRequest(`/session/${sessionId}/keys`, {
327921
+ method: "POST",
327922
+ body: JSON.stringify({ text: value, value: Array.from(value) })
327923
+ });
327924
+ }
327925
+ async function trySendElementValue(sessionId, elementId, value) {
327926
+ try {
327927
+ await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
327928
+ method: "POST",
327929
+ body: JSON.stringify({ text: value })
327930
+ });
327931
+ return null;
327932
+ } catch (error2) {
327933
+ if (isTransportError(error2)) throw error2;
327934
+ return error2;
327935
+ }
327936
+ }
327937
+ function shouldAbortAfterStepFailure(action) {
327938
+ switch (action) {
327939
+ case "launchApp":
327940
+ case "tap":
327941
+ case "type":
327942
+ case "clear":
327943
+ case "swipe":
327944
+ case "scroll":
327945
+ case "back":
327946
+ case "home":
327947
+ case "waitForElement":
327948
+ return true;
327949
+ case "assertVisible":
327950
+ case "assertText":
327951
+ return false;
327952
+ }
327953
+ }
327664
327954
  async function executeTap(sessionId, step) {
327665
327955
  const bounds = boundsFromStep(step);
327666
327956
  if (step.target) {
@@ -327682,30 +327972,64 @@ async function executeTap(sessionId, step) {
327682
327972
  }
327683
327973
  async function executeType(sessionId, step) {
327684
327974
  if (step.value === void 0 || step.value === null || step.value === "") return;
327975
+ const value = String(step.value);
327685
327976
  const bounds = boundsFromStep(step);
327686
327977
  let elementId = null;
327978
+ let lastInputError = null;
327979
+ let tappedBounds = false;
327980
+ if (bounds) {
327981
+ await tapCoordinates(sessionId, bounds);
327982
+ tappedBounds = true;
327983
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
327984
+ elementId = await getActiveElementId(sessionId);
327985
+ if (elementId) {
327986
+ lastInputError = await trySendElementValue(sessionId, elementId, value);
327987
+ if (!lastInputError) return;
327988
+ elementId = null;
327989
+ }
327990
+ }
327687
327991
  if (step.target) {
327688
327992
  try {
327689
327993
  elementId = await findElement(sessionId, step.target, readStepTimeout(step));
327994
+ lastInputError = await trySendElementValue(sessionId, elementId, value);
327995
+ if (!lastInputError) return;
327996
+ elementId = null;
327690
327997
  } catch (error2) {
327691
327998
  if (!bounds) throw error2;
327999
+ lastInputError = error2;
327692
328000
  }
327693
328001
  }
327694
328002
  if (!elementId && bounds) {
327695
328003
  await tapCoordinates(sessionId, bounds);
328004
+ tappedBounds = true;
327696
328005
  await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
327697
328006
  elementId = await getActiveElementId(sessionId);
327698
328007
  }
327699
328008
  if (!elementId) {
327700
328009
  elementId = await getActiveElementId(sessionId);
327701
328010
  }
327702
- if (!elementId) {
327703
- throw new Error("TYPE step could not resolve a target or focused element");
328011
+ if (elementId) {
328012
+ lastInputError = await trySendElementValue(sessionId, elementId, value);
328013
+ if (!lastInputError) return;
328014
+ elementId = null;
328015
+ if (!bounds) {
328016
+ throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
328017
+ }
327704
328018
  }
327705
- await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
327706
- method: "POST",
327707
- body: JSON.stringify({ text: step.value })
327708
- });
328019
+ if (bounds) {
328020
+ if (!tappedBounds) {
328021
+ await tapCoordinates(sessionId, bounds);
328022
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
328023
+ }
328024
+ try {
328025
+ await sendKeysToFocusedInput(sessionId, value);
328026
+ return;
328027
+ } catch (error2) {
328028
+ if (isTransportError(error2)) throw error2;
328029
+ lastInputError = error2;
328030
+ }
328031
+ }
328032
+ throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
327709
328033
  }
327710
328034
  async function executeClear(sessionId, step) {
327711
328035
  const bounds = boundsFromStep(step);
@@ -327909,6 +328233,7 @@ async function executeMobileTest(options) {
327909
328233
  log2(` \u2717 Deep link failed: ${err instanceof Error ? err.message : String(err)}`);
327910
328234
  }
327911
328235
  }
328236
+ await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
327912
328237
  let environmental = null;
327913
328238
  for (const step of options.steps) {
327914
328239
  const stepStart = Date.now();
@@ -327916,6 +328241,7 @@ async function executeMobileTest(options) {
327916
328241
  let status = "passed";
327917
328242
  let stepError;
327918
328243
  try {
328244
+ await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} pre-check`, false);
327919
328245
  switch (step.action) {
327920
328246
  case "launchApp":
327921
328247
  log2(" App launched (handled by session creation)");
@@ -327955,6 +328281,7 @@ async function executeMobileTest(options) {
327955
328281
  break;
327956
328282
  }
327957
328283
  }
328284
+ await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
327958
328285
  } catch (err) {
327959
328286
  status = "failed";
327960
328287
  stepError = err instanceof Error ? err.message : String(err);
@@ -327975,6 +328302,10 @@ async function executeMobileTest(options) {
327975
328302
  log2(`Aborting remaining steps \u2014 Appium session is no longer usable: ${environmental}`);
327976
328303
  break;
327977
328304
  }
328305
+ if (status === "failed" && shouldAbortAfterStepFailure(step.action)) {
328306
+ log2(`Aborting remaining steps \u2014 ${step.action} failed, so downstream mobile steps are no longer reliable.`);
328307
+ break;
328308
+ }
327978
328309
  }
327979
328310
  const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
327980
328311
  const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
@@ -329240,7 +329571,7 @@ function detachMirrorSession(sessionId) {
329240
329571
  }
329241
329572
 
329242
329573
  // src/services/mobile/mobile-bridge-driver.ts
329243
- var import_runner_core8 = __toESM(require_dist2());
329574
+ var import_runner_core9 = __toESM(require_dist2());
329244
329575
  init_dist();
329245
329576
  var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
329246
329577
  function readStringValue(result) {
@@ -329250,7 +329581,7 @@ function readStringValue(result) {
329250
329581
  }
329251
329582
  return void 0;
329252
329583
  }
329253
- function normalizeAndroidActivity(activity, pkg2) {
329584
+ function normalizeAndroidActivity2(activity, pkg2) {
329254
329585
  const trimmed = activity?.trim();
329255
329586
  if (!trimmed) return void 0;
329256
329587
  const packageName = pkg2?.trim();
@@ -329286,7 +329617,7 @@ var MobileBridgeDriver = class {
329286
329617
  return this.platform === "ANDROID" ? { appId: this.appId } : { bundleId: this.appId };
329287
329618
  }
329288
329619
  baseScreenSignal(source) {
329289
- const parsed = (0, import_runner_core8.parseMobileSource)(source, this.platform);
329620
+ const parsed = (0, import_runner_core9.parseMobileSource)(source, this.platform);
329290
329621
  return {
329291
329622
  platform: this.platform,
329292
329623
  screenHash: parsed.screenSignal.screenHash,
@@ -329305,11 +329636,11 @@ var MobileBridgeDriver = class {
329305
329636
  const activity = readStringValue(activityRes);
329306
329637
  const pkg2 = readStringValue(packageRes);
329307
329638
  if (pkg2) signal.bundleId = pkg2;
329308
- const normalizedActivity = normalizeAndroidActivity(activity, pkg2);
329639
+ const normalizedActivity = normalizeAndroidActivity2(activity, pkg2);
329309
329640
  if (normalizedActivity) signal.activity = normalizedActivity;
329310
329641
  } else {
329311
329642
  const infoRes = await this.executeScript("mobile: activeAppInfo", []).catch(() => void 0);
329312
- const bundleId = readActiveBundleId(infoRes);
329643
+ const bundleId = readActiveBundleId2(infoRes);
329313
329644
  if (bundleId) signal.bundleId = bundleId;
329314
329645
  }
329315
329646
  } catch {
@@ -329451,7 +329782,7 @@ var MobileBridgeDriver = class {
329451
329782
  return this.enrichScreenSignal(source);
329452
329783
  }
329453
329784
  };
329454
- function readActiveBundleId(result) {
329785
+ function readActiveBundleId2(result) {
329455
329786
  if (result && typeof result === "object" && "value" in result) {
329456
329787
  const value = result.value;
329457
329788
  if (value && typeof value === "object" && "bundleId" in value) {
@@ -329677,7 +330008,7 @@ function printDoctorReport(report) {
329677
330008
 
329678
330009
  // src/runner.ts
329679
330010
  init_dist();
329680
- var import_runner_core11 = __toESM(require_dist2());
330011
+ var import_runner_core12 = __toESM(require_dist2());
329681
330012
  function fingerprintTest(test) {
329682
330013
  const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
329683
330014
  return (0, import_node_crypto2.createHash)("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
@@ -330122,7 +330453,7 @@ function pruneCollectedPageForPost(page) {
330122
330453
  if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
330123
330454
  return out;
330124
330455
  }
330125
- function prepareDiscoveryResultForPost(result, logs2) {
330456
+ function prepareDiscoveryResultForPost(result, logs2, options = {}) {
330126
330457
  const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
330127
330458
  if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
330128
330459
  if (Array.isArray(safe.apiCalls)) safe.apiCalls = safe.apiCalls.map((call) => compactForAudit(call));
@@ -330138,6 +330469,10 @@ function prepareDiscoveryResultForPost(result, logs2) {
330138
330469
  if (safe.snapshotEvidence) safe.snapshotEvidence = compactForAudit(safe.snapshotEvidence);
330139
330470
  if (safe.discoveryCheckpoints) safe.discoveryCheckpoints = compactForAudit(safe.discoveryCheckpoints);
330140
330471
  if (safe.pageScreenshotKeys && safe.pageScreenshots) delete safe.pageScreenshots;
330472
+ if (options.omitScreenshots) {
330473
+ if (Array.isArray(safe.screenshots)) delete safe.screenshots;
330474
+ if (safe.pageScreenshots) delete safe.pageScreenshots;
330475
+ }
330141
330476
  if (safe.exploreAlreadySaved === true) {
330142
330477
  if (Array.isArray(safe.screenshots)) delete safe.screenshots;
330143
330478
  if (safe.pageScreenshots) delete safe.pageScreenshots;
@@ -330237,7 +330572,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
330237
330572
  const runMode = run2.mode ?? "run";
330238
330573
  const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
330239
330574
  log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
330240
- const liveBrowserHandle = import_runner_core10.liveBrowserRegistry.register({
330575
+ const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
330241
330576
  sessionId: `${run2.runId}`,
330242
330577
  mode: toLiveBrowserMode(runMode),
330243
330578
  runId: run2.runId,
@@ -330246,7 +330581,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
330246
330581
  testCaseId: run2.testCaseId ?? void 0
330247
330582
  });
330248
330583
  requestLiveBrowserHeartbeat(true);
330249
- const liveBrowserHeartbeatInterval = setInterval(() => requestLiveBrowserHeartbeat(), 5e3);
330584
+ const liveBrowserHeartbeatInterval = setInterval(() => {
330585
+ liveBrowserHandle.patch({});
330586
+ requestLiveBrowserHeartbeat();
330587
+ }, 5e3);
330250
330588
  liveBrowserHeartbeatInterval.unref?.();
330251
330589
  const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
330252
330590
  ...nativeOptions,
@@ -330273,7 +330611,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
330273
330611
  log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
330274
330612
  return;
330275
330613
  }
330276
- const preflight = await (0, import_runner_core11.resolveBaseUrlPreflight)(run2.baseUrl);
330614
+ const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
330277
330615
  if (preflight.error) {
330278
330616
  await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
330279
330617
  log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
@@ -330330,7 +330668,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
330330
330668
  return;
330331
330669
  }
330332
330670
  const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
330333
- const batchResult = await (0, import_runner_core9.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
330671
+ const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
330334
330672
  testVariables: run2.testVariables,
330335
330673
  runNativeTest: runNativeWithLiveBrowser,
330336
330674
  validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
@@ -330351,16 +330689,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
330351
330689
  }
330352
330690
  return;
330353
330691
  }
330354
- if (runMode === "heal") {
330355
- if (run2.framework === "APPIUM") {
330356
- await postResult(opts.serverUrl, headers, run2.runId, {
330357
- status: "ERROR",
330358
- error: "CONFIG_ISSUE: Appium (mobile) tests are not auto-healable; skipped heal.",
330359
- duration: Date.now() - startTime
330360
- });
330361
- log.warn("Skipped heal for APPIUM test (mobile tests are not healable)");
330362
- return;
330363
- }
330692
+ if (runMode === "heal" && run2.framework !== "APPIUM") {
330364
330693
  const isApiTest2 = Array.isArray(run2.tags) && run2.tags.includes("@api");
330365
330694
  if (isApiTest2) {
330366
330695
  const testCode = run2.playwrightCode ?? "";
@@ -330564,7 +330893,7 @@ ${finalVerifyResult.logs ?? ""}`;
330564
330893
  if (classifyErrorMessage(composedError ?? null) === "config_issue") {
330565
330894
  apiHealStatus = "ERROR";
330566
330895
  } else {
330567
- const rlDetection = (0, import_runner_core11.detectRateLimitBlock)({
330896
+ const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
330568
330897
  testName: run2.testName ?? run2.testCaseId ?? void 0,
330569
330898
  tags: run2.tags ?? [],
330570
330899
  error: composedError ?? finalVerifyResult?.error ?? null,
@@ -330671,7 +331000,7 @@ ${finalVerifyResult.logs ?? ""}`;
330671
331000
  log.info(`Heal engine: GROK (${source})`);
330672
331001
  }
330673
331002
  const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
330674
- const result2 = useGrokHeal ? await (0, import_runner_core9.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
331003
+ const result2 = useGrokHeal ? await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
330675
331004
  ...healOpts,
330676
331005
  onLog: grokHealLogStream.onLog
330677
331006
  }) : await (0, import_runner_core2.executeTestWithHealing)(run2.playwrightCode, run2.steps, run2.baseUrl, healOpts);
@@ -330704,7 +331033,7 @@ ${finalVerifyResult.logs ?? ""}`;
330704
331033
  let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
330705
331034
  let healError = result2.error;
330706
331035
  if (!result2.passed && !isConfigIssue) {
330707
- const rlDetection = (0, import_runner_core11.detectRateLimitBlock)({
331036
+ const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
330708
331037
  testName: run2.testName ?? run2.testCaseId ?? void 0,
330709
331038
  tags: run2.tags ?? [],
330710
331039
  error: result2.error,
@@ -330898,7 +331227,7 @@ ${finalVerifyResult.logs ?? ""}`;
330898
331227
  liveBrowserHandle,
330899
331228
  runNativeTest: runNativeWithLiveBrowser
330900
331229
  };
330901
- const healResult = useGrokHealSuite ? await (0, import_runner_core9.executeGrokPerTestHeal)(
331230
+ const healResult = useGrokHealSuite ? await (0, import_runner_core10.executeGrokPerTestHeal)(
330902
331231
  failedTest.playwrightCode,
330903
331232
  failedTest.steps ?? [],
330904
331233
  run2.baseUrl,
@@ -331186,9 +331515,16 @@ ${finalVerifyResult.logs ?? ""}`;
331186
331515
  signal: AbortSignal.timeout(3e4)
331187
331516
  });
331188
331517
  if (res.ok) return;
331189
- log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${res.status}`);
331518
+ const responseText = await res.text().catch(() => "");
331519
+ const statusMessage = responseText ? `${res.status}: ${responseText.slice(0, 500)}` : String(res.status);
331520
+ if (res.status === 409) {
331521
+ throw new Error(`Discovery plan checkpoint rejected: ${statusMessage}`);
331522
+ }
331523
+ log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${statusMessage}`);
331190
331524
  } catch (err) {
331191
- log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
331525
+ const message = err instanceof Error ? err.message : String(err);
331526
+ if (/Discovery plan checkpoint rejected/i.test(message)) throw err;
331527
+ log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${message}`);
331192
331528
  }
331193
331529
  if (attempt < 2) await new Promise((r) => setTimeout(r, 3e3 * (attempt + 1)));
331194
331530
  }
@@ -331196,6 +331532,7 @@ ${finalVerifyResult.logs ?? ""}`;
331196
331532
  };
331197
331533
  let discoveryResult;
331198
331534
  const buildFinalBody = (r, overrideError, auditWarning) => {
331535
+ const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
331199
331536
  const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
331200
331537
  void _exploreFinalMessages;
331201
331538
  const logs2 = auditWarning ? [rest.logs, auditWarning].filter(Boolean).join("\n") : rest.logs;
@@ -331245,7 +331582,7 @@ ${finalVerifyResult.logs ?? ""}`;
331245
331582
  resolvedAppOrigin: rest.resolvedAppOrigin,
331246
331583
  plans: rest.plans,
331247
331584
  exploreStats: rest.exploreStats,
331248
- screenshots: rest.screenshots,
331585
+ ...isNativeDiscovery ? {} : { screenshots: rest.screenshots },
331249
331586
  snapshotEvidence: rest.snapshotEvidence,
331250
331587
  discoveryCheckpoints: rest.discoveryCheckpoints,
331251
331588
  streamedTestCount: streamedTestKeys.size,
@@ -331254,8 +331591,10 @@ ${finalVerifyResult.logs ?? ""}`;
331254
331591
  ...explorePosted ? {} : {
331255
331592
  collectedPages: rest.collectedPages,
331256
331593
  collectedTransitions: rest.collectedTransitions,
331257
- pageScreenshots: rest.pageScreenshots,
331258
- pageScreenshotKeys: rest.pageScreenshotKeys
331594
+ ...isNativeDiscovery ? {} : {
331595
+ pageScreenshots: rest.pageScreenshots,
331596
+ pageScreenshotKeys: rest.pageScreenshotKeys
331597
+ }
331259
331598
  },
331260
331599
  ...harPosted ? {} : {
331261
331600
  apiCalls: rest.apiCalls,
@@ -331263,7 +331602,7 @@ ${finalVerifyResult.logs ?? ""}`;
331263
331602
  },
331264
331603
  // Only send tests that were NOT confirmed streamed to avoid duplicates
331265
331604
  tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
331266
- }, logs2);
331605
+ }, logs2, { omitScreenshots: isNativeDiscovery });
331267
331606
  };
331268
331607
  const acquireLoginBrowser = async () => {
331269
331608
  const browser = await browserPool.acquireAsync(12e3);
@@ -331367,7 +331706,6 @@ ${finalVerifyResult.logs ?? ""}`;
331367
331706
  thumbnailCapturedAt: Date.now()
331368
331707
  });
331369
331708
  requestLiveBrowserHeartbeat();
331370
- void onScreenshot("mobile", base64);
331371
331709
  },
331372
331710
  onExploreComplete,
331373
331711
  onTestGenerated,
@@ -331394,7 +331732,7 @@ ${finalVerifyResult.logs ?? ""}`;
331394
331732
  setExecutorBusy(false);
331395
331733
  }
331396
331734
  } else if (ctx.mode === "import") {
331397
- const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core9.runRepoAnalysisOnRunner : import_runner_core9.runFrameworkImportOnRunner;
331735
+ const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_runner_core10.runFrameworkImportOnRunner;
331398
331736
  discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
331399
331737
  archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
331400
331738
  archiveAuthHeader: headers.Authorization,
@@ -331724,7 +332062,7 @@ ${finalVerifyResult.logs ?? ""}`;
331724
332062
  let runStatus = result.passed ? "PASSED" : "FAILED";
331725
332063
  let runError = result.error;
331726
332064
  if (!result.passed) {
331727
- const rlDetection = (0, import_runner_core11.detectRateLimitBlock)({
332065
+ const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
331728
332066
  testName: run2.testName ?? run2.testCaseId ?? void 0,
331729
332067
  tags: run2.tags ?? [],
331730
332068
  error: result.error,
@@ -331862,7 +332200,7 @@ async function startRunner(opts) {
331862
332200
  capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
331863
332201
  run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
331864
332202
  };
331865
- const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core11.createResourceGovernorOptions)({
332203
+ const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
331866
332204
  isCloud: IS_CLOUD,
331867
332205
  maxConcurrent: MAX_CONCURRENT,
331868
332206
  typeLimits: TYPE_LIMITS
@@ -332013,8 +332351,8 @@ async function startRunner(opts) {
332013
332351
  android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
332014
332352
  } : { web: true, ios: false, android: false };
332015
332353
  const bridge = enableMobile ? getBridgeState() : void 0;
332016
- const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
332017
- const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
332354
+ const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
332355
+ const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
332018
332356
  lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
332019
332357
  lastInventoryKinds.clear();
332020
332358
  for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
@@ -332041,8 +332379,8 @@ async function startRunner(opts) {
332041
332379
  resourceSnapshot: resourceStatus.snapshot,
332042
332380
  resourceGovernor: resourceStatus.governor,
332043
332381
  processInventory,
332044
- aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
332045
- liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
332382
+ aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
332383
+ liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
332046
332384
  // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
332047
332385
  // Cached & cheap — see getCachedDoctor above.
332048
332386
  doctor: getCachedDoctor()
@@ -332136,7 +332474,7 @@ async function startRunner(opts) {
332136
332474
  let pidsWatchdogWarned = false;
332137
332475
  const pidsWatchdog = setInterval(() => {
332138
332476
  if (shuttingDown) return;
332139
- const snapshot = (0, import_runner_core11.getResourceSnapshot)();
332477
+ const snapshot = (0, import_runner_core12.getResourceSnapshot)();
332140
332478
  const pct = snapshot.pidsPct;
332141
332479
  if (pct == null || !Number.isFinite(pct)) return;
332142
332480
  if (pct >= PIDS_RECYCLE_PCT) {
@@ -332172,7 +332510,7 @@ async function startRunner(opts) {
332172
332510
  log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
332173
332511
  continue;
332174
332512
  }
332175
- const admission = (0, import_runner_core11.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
332513
+ const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
332176
332514
  mode: runMode,
332177
332515
  activeRuns,
332178
332516
  activeByType: Object.fromEntries(activeByType),