@validate.qa/runner 1.0.2 → 1.0.3

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 +407 -330
  2. package/dist/cli.mjs +412 -335
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1405,7 +1405,7 @@ function selectorForValue(value) {
1405
1405
  if (value.startsWith("~") || value.startsWith("id="))
1406
1406
  return value;
1407
1407
  if (value.includes(":id/"))
1408
- return `id=${value.split(":id/")[1]}`;
1408
+ return `id=${value}`;
1409
1409
  return `~${value}`;
1410
1410
  }
1411
1411
  function selectorCandidatesForStep(step) {
@@ -21738,7 +21738,7 @@ var require_util = __commonJS({
21738
21738
  return path;
21739
21739
  }
21740
21740
  exports2.normalize = normalize;
21741
- function join4(aRoot, aPath) {
21741
+ function join5(aRoot, aPath) {
21742
21742
  if (aRoot === "") {
21743
21743
  aRoot = ".";
21744
21744
  }
@@ -21770,7 +21770,7 @@ var require_util = __commonJS({
21770
21770
  }
21771
21771
  return joined;
21772
21772
  }
21773
- exports2.join = join4;
21773
+ exports2.join = join5;
21774
21774
  exports2.isAbsolute = function(aPath) {
21775
21775
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
21776
21776
  };
@@ -21943,7 +21943,7 @@ var require_util = __commonJS({
21943
21943
  parsed.path = parsed.path.substring(0, index + 1);
21944
21944
  }
21945
21945
  }
21946
- sourceURL = join4(urlGenerate(parsed), sourceURL);
21946
+ sourceURL = join5(urlGenerate(parsed), sourceURL);
21947
21947
  }
21948
21948
  return normalize(sourceURL);
21949
21949
  }
@@ -243098,7 +243098,7 @@ var require_thread_stream = __commonJS({
243098
243098
  var { version } = require_package();
243099
243099
  var { EventEmitter: EventEmitter2 } = require("events");
243100
243100
  var { Worker } = require("worker_threads");
243101
- var { join: join4 } = require("path");
243101
+ var { join: join5 } = require("path");
243102
243102
  var { pathToFileURL } = require("url");
243103
243103
  var { wait } = require_wait();
243104
243104
  var {
@@ -243149,7 +243149,7 @@ var require_thread_stream = __commonJS({
243149
243149
  function createWorker(stream, opts) {
243150
243150
  const { filename, workerData } = opts;
243151
243151
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
243152
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
243152
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
243153
243153
  const worker = new Worker(toExecute, {
243154
243154
  ...opts.workerOpts,
243155
243155
  name: opts.workerOpts?.name || "thread-stream",
@@ -243617,7 +243617,7 @@ var require_transport = __commonJS({
243617
243617
  var { createRequire } = require("module");
243618
243618
  var { existsSync: existsSync4 } = require("fs");
243619
243619
  var getCallers = require_caller();
243620
- var { join: join4, isAbsolute, sep } = require("path");
243620
+ var { join: join5, isAbsolute, sep } = require("path");
243621
243621
  var { fileURLToPath } = require("url");
243622
243622
  var sleep = require_atomic_sleep();
243623
243623
  var onExit = require_on_exit_leak_free();
@@ -243770,7 +243770,7 @@ var require_transport = __commonJS({
243770
243770
  throw new Error("only one of target or targets can be specified");
243771
243771
  }
243772
243772
  if (targets) {
243773
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243773
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243774
243774
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
243775
243775
  return {
243776
243776
  ...dest,
@@ -243788,7 +243788,7 @@ var require_transport = __commonJS({
243788
243788
  });
243789
243789
  });
243790
243790
  } else if (pipeline) {
243791
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243791
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243792
243792
  options.pipelines = [pipeline.map((dest) => {
243793
243793
  return {
243794
243794
  ...dest,
@@ -243811,7 +243811,7 @@ var require_transport = __commonJS({
243811
243811
  return origin;
243812
243812
  }
243813
243813
  if (origin === "pino/file") {
243814
- return join4(__dirname, "..", "file.js");
243814
+ return join5(__dirname, "..", "file.js");
243815
243815
  }
243816
243816
  let fixTarget2;
243817
243817
  for (const filePath of callers) {
@@ -244792,7 +244792,7 @@ var require_safe_stable_stringify = __commonJS({
244792
244792
  return circularValue;
244793
244793
  }
244794
244794
  let res = "";
244795
- let join4 = ",";
244795
+ let join5 = ",";
244796
244796
  const originalIndentation = indentation;
244797
244797
  if (Array.isArray(value)) {
244798
244798
  if (value.length === 0) {
@@ -244806,7 +244806,7 @@ var require_safe_stable_stringify = __commonJS({
244806
244806
  indentation += spacer;
244807
244807
  res += `
244808
244808
  ${indentation}`;
244809
- join4 = `,
244809
+ join5 = `,
244810
244810
  ${indentation}`;
244811
244811
  }
244812
244812
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244814,13 +244814,13 @@ ${indentation}`;
244814
244814
  for (; i < maximumValuesToStringify - 1; i++) {
244815
244815
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244816
244816
  res += tmp2 !== void 0 ? tmp2 : "null";
244817
- res += join4;
244817
+ res += join5;
244818
244818
  }
244819
244819
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244820
244820
  res += tmp !== void 0 ? tmp : "null";
244821
244821
  if (value.length - 1 > maximumBreadth) {
244822
244822
  const removedKeys = value.length - maximumBreadth - 1;
244823
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244823
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244824
244824
  }
244825
244825
  if (spacer !== "") {
244826
244826
  res += `
@@ -244841,7 +244841,7 @@ ${originalIndentation}`;
244841
244841
  let separator = "";
244842
244842
  if (spacer !== "") {
244843
244843
  indentation += spacer;
244844
- join4 = `,
244844
+ join5 = `,
244845
244845
  ${indentation}`;
244846
244846
  whitespace = " ";
244847
244847
  }
@@ -244855,13 +244855,13 @@ ${indentation}`;
244855
244855
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
244856
244856
  if (tmp !== void 0) {
244857
244857
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244858
- separator = join4;
244858
+ separator = join5;
244859
244859
  }
244860
244860
  }
244861
244861
  if (keyLength > maximumBreadth) {
244862
244862
  const removedKeys = keyLength - maximumBreadth;
244863
244863
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
244864
- separator = join4;
244864
+ separator = join5;
244865
244865
  }
244866
244866
  if (spacer !== "" && separator.length > 1) {
244867
244867
  res = `
@@ -244902,7 +244902,7 @@ ${originalIndentation}`;
244902
244902
  }
244903
244903
  const originalIndentation = indentation;
244904
244904
  let res = "";
244905
- let join4 = ",";
244905
+ let join5 = ",";
244906
244906
  if (Array.isArray(value)) {
244907
244907
  if (value.length === 0) {
244908
244908
  return "[]";
@@ -244915,7 +244915,7 @@ ${originalIndentation}`;
244915
244915
  indentation += spacer;
244916
244916
  res += `
244917
244917
  ${indentation}`;
244918
- join4 = `,
244918
+ join5 = `,
244919
244919
  ${indentation}`;
244920
244920
  }
244921
244921
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244923,13 +244923,13 @@ ${indentation}`;
244923
244923
  for (; i < maximumValuesToStringify - 1; i++) {
244924
244924
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244925
244925
  res += tmp2 !== void 0 ? tmp2 : "null";
244926
- res += join4;
244926
+ res += join5;
244927
244927
  }
244928
244928
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244929
244929
  res += tmp !== void 0 ? tmp : "null";
244930
244930
  if (value.length - 1 > maximumBreadth) {
244931
244931
  const removedKeys = value.length - maximumBreadth - 1;
244932
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244932
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244933
244933
  }
244934
244934
  if (spacer !== "") {
244935
244935
  res += `
@@ -244942,7 +244942,7 @@ ${originalIndentation}`;
244942
244942
  let whitespace = "";
244943
244943
  if (spacer !== "") {
244944
244944
  indentation += spacer;
244945
- join4 = `,
244945
+ join5 = `,
244946
244946
  ${indentation}`;
244947
244947
  whitespace = " ";
244948
244948
  }
@@ -244951,7 +244951,7 @@ ${indentation}`;
244951
244951
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
244952
244952
  if (tmp !== void 0) {
244953
244953
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244954
- separator = join4;
244954
+ separator = join5;
244955
244955
  }
244956
244956
  }
244957
244957
  if (spacer !== "" && separator.length > 1) {
@@ -245009,20 +245009,20 @@ ${originalIndentation}`;
245009
245009
  indentation += spacer;
245010
245010
  let res2 = `
245011
245011
  ${indentation}`;
245012
- const join5 = `,
245012
+ const join6 = `,
245013
245013
  ${indentation}`;
245014
245014
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
245015
245015
  let i = 0;
245016
245016
  for (; i < maximumValuesToStringify - 1; i++) {
245017
245017
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245018
245018
  res2 += tmp2 !== void 0 ? tmp2 : "null";
245019
- res2 += join5;
245019
+ res2 += join6;
245020
245020
  }
245021
245021
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245022
245022
  res2 += tmp !== void 0 ? tmp : "null";
245023
245023
  if (value.length - 1 > maximumBreadth) {
245024
245024
  const removedKeys = value.length - maximumBreadth - 1;
245025
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
245025
+ res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
245026
245026
  }
245027
245027
  res2 += `
245028
245028
  ${originalIndentation}`;
@@ -245038,16 +245038,16 @@ ${originalIndentation}`;
245038
245038
  return '"[Object]"';
245039
245039
  }
245040
245040
  indentation += spacer;
245041
- const join4 = `,
245041
+ const join5 = `,
245042
245042
  ${indentation}`;
245043
245043
  let res = "";
245044
245044
  let separator = "";
245045
245045
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
245046
245046
  if (isTypedArrayWithEntries(value)) {
245047
- res += stringifyTypedArray(value, join4, maximumBreadth);
245047
+ res += stringifyTypedArray(value, join5, maximumBreadth);
245048
245048
  keys = keys.slice(value.length);
245049
245049
  maximumPropertiesToStringify -= value.length;
245050
- separator = join4;
245050
+ separator = join5;
245051
245051
  }
245052
245052
  if (deterministic) {
245053
245053
  keys = sort(keys, comparator);
@@ -245058,13 +245058,13 @@ ${indentation}`;
245058
245058
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
245059
245059
  if (tmp !== void 0) {
245060
245060
  res += `${separator}${strEscape(key2)}: ${tmp}`;
245061
- separator = join4;
245061
+ separator = join5;
245062
245062
  }
245063
245063
  }
245064
245064
  if (keyLength > maximumBreadth) {
245065
245065
  const removedKeys = keyLength - maximumBreadth;
245066
245066
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
245067
- separator = join4;
245067
+ separator = join5;
245068
245068
  }
245069
245069
  if (separator !== "") {
245070
245070
  res = `
@@ -285421,11 +285421,11 @@ var require_core = __commonJS({
285421
285421
  function mapOuter(r, f) {
285422
285422
  return r.matched ? f(r) : r;
285423
285423
  }
285424
- function ab(pa, pb, join4) {
285425
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join4(ma.value, vb, data, i, j)));
285424
+ function ab(pa, pb, join5) {
285425
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
285426
285426
  }
285427
- function abc(pa, pb, pc, join4) {
285428
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join4(ma.value, mb.value, vc, data, i, j))));
285427
+ function abc(pa, pb, pc, join5) {
285428
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join5(ma.value, mb.value, vc, data, i, j))));
285429
285429
  }
285430
285430
  function action(f) {
285431
285431
  return (data, i) => {
@@ -312619,7 +312619,7 @@ var require_utils2 = __commonJS({
312619
312619
  module2.exports = Utils;
312620
312620
  Utils.prototype.makeDir = function(folder) {
312621
312621
  const self = this;
312622
- function mkdirSync3(fpath) {
312622
+ function mkdirSync4(fpath) {
312623
312623
  let resolvedPath = fpath.split(self.sep)[0];
312624
312624
  fpath.split(self.sep).forEach(function(name) {
312625
312625
  if (!name || name.substr(-1, 1) === ":") return;
@@ -312637,7 +312637,7 @@ var require_utils2 = __commonJS({
312637
312637
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
312638
312638
  });
312639
312639
  }
312640
- mkdirSync3(folder);
312640
+ mkdirSync4(folder);
312641
312641
  };
312642
312642
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
312643
312643
  const self = this;
@@ -314256,8 +314256,8 @@ var require_adm_zip = __commonJS({
314256
314256
  return null;
314257
314257
  }
314258
314258
  function fixPath(zipPath) {
314259
- const { join: join4, normalize, sep } = pth.posix;
314260
- return join4(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314259
+ const { join: join5, normalize, sep } = pth.posix;
314260
+ return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314261
314261
  }
314262
314262
  function filenameFilter(filterfn) {
314263
314263
  if (filterfn instanceof RegExp) {
@@ -319670,14 +319670,14 @@ var require_path_arg = __commonJS({
319670
319670
  var require_find_made = __commonJS({
319671
319671
  "../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports2, module2) {
319672
319672
  "use strict";
319673
- var { dirname } = require("path");
319673
+ var { dirname: dirname2 } = require("path");
319674
319674
  var findMade = (opts, parent, path = void 0) => {
319675
319675
  if (path === parent)
319676
319676
  return Promise.resolve();
319677
319677
  return opts.statAsync(parent).then(
319678
319678
  (st) => st.isDirectory() ? path : void 0,
319679
319679
  // will fail later
319680
- (er) => er.code === "ENOENT" ? findMade(opts, dirname(parent), parent) : void 0
319680
+ (er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
319681
319681
  );
319682
319682
  };
319683
319683
  var findMadeSync = (opts, parent, path = void 0) => {
@@ -319686,7 +319686,7 @@ var require_find_made = __commonJS({
319686
319686
  try {
319687
319687
  return opts.statSync(parent).isDirectory() ? path : void 0;
319688
319688
  } catch (er) {
319689
- return er.code === "ENOENT" ? findMadeSync(opts, dirname(parent), parent) : void 0;
319689
+ return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
319690
319690
  }
319691
319691
  };
319692
319692
  module2.exports = { findMade, findMadeSync };
@@ -319697,10 +319697,10 @@ var require_find_made = __commonJS({
319697
319697
  var require_mkdirp_manual = __commonJS({
319698
319698
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports2, module2) {
319699
319699
  "use strict";
319700
- var { dirname } = require("path");
319700
+ var { dirname: dirname2 } = require("path");
319701
319701
  var mkdirpManual = (path, opts, made) => {
319702
319702
  opts.recursive = false;
319703
- const parent = dirname(path);
319703
+ const parent = dirname2(path);
319704
319704
  if (parent === path) {
319705
319705
  return opts.mkdirAsync(path, opts).catch((er) => {
319706
319706
  if (er.code !== "EISDIR")
@@ -319723,7 +319723,7 @@ var require_mkdirp_manual = __commonJS({
319723
319723
  });
319724
319724
  };
319725
319725
  var mkdirpManualSync = (path, opts, made) => {
319726
- const parent = dirname(path);
319726
+ const parent = dirname2(path);
319727
319727
  opts.recursive = false;
319728
319728
  if (parent === path) {
319729
319729
  try {
@@ -319759,12 +319759,12 @@ var require_mkdirp_manual = __commonJS({
319759
319759
  var require_mkdirp_native = __commonJS({
319760
319760
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports2, module2) {
319761
319761
  "use strict";
319762
- var { dirname } = require("path");
319762
+ var { dirname: dirname2 } = require("path");
319763
319763
  var { findMade, findMadeSync } = require_find_made();
319764
319764
  var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
319765
319765
  var mkdirpNative = (path, opts) => {
319766
319766
  opts.recursive = true;
319767
- const parent = dirname(path);
319767
+ const parent = dirname2(path);
319768
319768
  if (parent === path)
319769
319769
  return opts.mkdirAsync(path, opts);
319770
319770
  return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
@@ -319776,7 +319776,7 @@ var require_mkdirp_native = __commonJS({
319776
319776
  };
319777
319777
  var mkdirpNativeSync = (path, opts) => {
319778
319778
  opts.recursive = true;
319779
- const parent = dirname(path);
319779
+ const parent = dirname2(path);
319780
319780
  if (parent === path)
319781
319781
  return opts.mkdirSync(path, opts);
319782
319782
  const made = findMadeSync(opts, path);
@@ -320181,7 +320181,7 @@ var require_path_reservations = __commonJS({
320181
320181
  var assert = require("assert");
320182
320182
  var normalize = require_normalize_unicode();
320183
320183
  var stripSlashes = require_strip_trailing_slashes();
320184
- var { join: join4 } = require("path");
320184
+ var { join: join5 } = require("path");
320185
320185
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
320186
320186
  var isWindows = platform3 === "win32";
320187
320187
  module2.exports = () => {
@@ -320190,7 +320190,7 @@ var require_path_reservations = __commonJS({
320190
320190
  const getDirs = (path) => {
320191
320191
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
320192
320192
  if (set.length) {
320193
- path2 = join4(set[set.length - 1], path2);
320193
+ path2 = join5(set[set.length - 1], path2);
320194
320194
  }
320195
320195
  set.push(path2 || "/");
320196
320196
  return set;
@@ -320258,7 +320258,7 @@ var require_path_reservations = __commonJS({
320258
320258
  };
320259
320259
  const reserve = (paths, fn) => {
320260
320260
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
320261
- return stripSlashes(join4(normalize(p))).toLowerCase();
320261
+ return stripSlashes(join5(normalize(p))).toLowerCase();
320262
320262
  });
320263
320263
  const dirs = new Set(
320264
320264
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -324796,7 +324796,7 @@ var require_package4 = __commonJS({
324796
324796
  "package.json"(exports2, module2) {
324797
324797
  module2.exports = {
324798
324798
  name: "@validate.qa/runner",
324799
- version: "1.0.2",
324799
+ version: "1.0.3",
324800
324800
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
324801
324801
  bin: {
324802
324802
  "validate-runner": "dist/cli.js"
@@ -324804,7 +324804,7 @@ var require_package4 = __commonJS({
324804
324804
  scripts: {
324805
324805
  build: "tsup",
324806
324806
  dev: "tsx src/cli.ts",
324807
- "test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
324807
+ "test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
324808
324808
  prepublishOnly: "npm run build"
324809
324809
  },
324810
324810
  files: [
@@ -324882,9 +324882,9 @@ function getConfigDir() {
324882
324882
  }
324883
324883
 
324884
324884
  // src/runner.ts
324885
- var import_fs2 = require("fs");
324886
- var import_path2 = require("path");
324887
- var import_os3 = require("os");
324885
+ var import_fs4 = require("fs");
324886
+ var import_path3 = require("path");
324887
+ var import_os5 = require("os");
324888
324888
  var import_node_crypto2 = require("crypto");
324889
324889
 
324890
324890
  // src/services/playwright-native.ts
@@ -325297,11 +325297,12 @@ function discoverRealIOSDevices() {
325297
325297
  for (const [index, node] of iosNodes.entries()) {
325298
325298
  const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
325299
325299
  const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
325300
+ const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
325300
325301
  devices.push({
325301
325302
  id: serial,
325302
325303
  name,
325303
325304
  platform: "IOS",
325304
- platformVersion: "unknown",
325305
+ platformVersion: version,
325305
325306
  state: "BOOTED",
325306
325307
  isAvailable: true,
325307
325308
  isPhysical: true
@@ -325376,7 +325377,7 @@ function parseAdbDevices() {
325376
325377
  const output = runCommand("adb", ["devices", "-l"]);
325377
325378
  if (!output) return [];
325378
325379
  const lines = output.split("\n").slice(1);
325379
- return lines.filter((line) => line.includes(" ") || line.includes("device ")).map((line) => {
325380
+ return lines.filter((line) => line.trim().length > 0).map((line) => {
325380
325381
  const parts = line.trim().split(/\s+/);
325381
325382
  const serial = parts[0];
325382
325383
  const type = parts[1] ?? "";
@@ -325392,26 +325393,25 @@ function parseAdbDevices() {
325392
325393
  device: props["device"],
325393
325394
  transportId: props["transport_id"]
325394
325395
  };
325395
- }).filter((d) => d.type === "device");
325396
+ }).filter((d) => d.type === "device" || d.type === "unauthorized");
325396
325397
  }
325397
325398
  function getAndroidProp(serial, prop) {
325398
325399
  return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
325399
325400
  }
325400
325401
  function discoverAndroidDevices() {
325401
325402
  const adbDevices = parseAdbDevices();
325402
- return adbDevices.filter((d) => {
325403
- return d.type === "device";
325404
- }).map((d) => {
325403
+ return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
325405
325404
  const isEmulator = d.serial.startsWith("emulator-");
325405
+ const isAvailable = d.type === "device";
325406
325406
  const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
325407
- const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
325407
+ const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
325408
325408
  return {
325409
325409
  id: d.serial,
325410
325410
  name,
325411
325411
  platform: "ANDROID",
325412
325412
  platformVersion,
325413
325413
  state: "BOOTED",
325414
- isAvailable: true,
325414
+ isAvailable,
325415
325415
  // Mark if it's a real device vs emulator
325416
325416
  ...isEmulator ? {} : { isPhysical: true }
325417
325417
  };
@@ -325444,10 +325444,14 @@ function discoverAllDevices(options) {
325444
325444
  const androidDevices = discoverAndroidDevices();
325445
325445
  if (options?.includeApps) {
325446
325446
  for (const device of iosDevices) {
325447
- device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325447
+ if (device.isAvailable) {
325448
+ device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325449
+ }
325448
325450
  }
325449
325451
  for (const device of androidDevices) {
325450
- device.installedApps = listAndroidInstalledApps(device.id);
325452
+ if (device.isAvailable) {
325453
+ device.installedApps = listAndroidInstalledApps(device.id);
325454
+ }
325451
325455
  }
325452
325456
  }
325453
325457
  return [...iosDevices, ...androidDevices];
@@ -325455,11 +325459,12 @@ function discoverAllDevices(options) {
325455
325459
  function getRunnerCapabilities() {
325456
325460
  const iosDevices = discoverIOSDevices();
325457
325461
  const androidDevices = discoverAndroidDevices();
325462
+ const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
325458
325463
  return {
325459
325464
  web: true,
325460
325465
  // Always capable of web testing via Playwright
325461
- ios: iosDevices.length > 0,
325462
- android: androidDevices.length > 0
325466
+ ios: iosDevices.some(isReadyDevice),
325467
+ android: androidDevices.some(isReadyDevice)
325463
325468
  };
325464
325469
  }
325465
325470
 
@@ -325861,7 +325866,7 @@ async function executeMobileTest(options) {
325861
325866
  const devices = discoverAllDevices({ includeApps: true });
325862
325867
  const platformFilter = options.target.platform;
325863
325868
  const matchingDevices = devices.filter(
325864
- (d) => d.platform === platformFilter && d.state === "BOOTED"
325869
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
325865
325870
  );
325866
325871
  if (matchingDevices.length === 0) {
325867
325872
  throw new Error(
@@ -326025,7 +326030,7 @@ async function createMobileDiscoverySession(target) {
326025
326030
  const devices = discoverAllDevices({ includeApps: true });
326026
326031
  const platformFilter = target.platform;
326027
326032
  const matchingDevices = devices.filter(
326028
- (d) => d.platform === platformFilter && d.state === "BOOTED"
326033
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
326029
326034
  );
326030
326035
  if (matchingDevices.length === 0) {
326031
326036
  throw new Error(
@@ -326076,6 +326081,9 @@ async function createMobileDiscoverySession(target) {
326076
326081
  var import_http = require("http");
326077
326082
  var import_child_process3 = require("child_process");
326078
326083
  var import_crypto = require("crypto");
326084
+ var import_os3 = require("os");
326085
+ var import_path2 = require("path");
326086
+ var import_fs2 = require("fs");
326079
326087
  init_dist();
326080
326088
 
326081
326089
  // src/services/mobile/appium-actions.ts
@@ -326361,30 +326369,50 @@ async function getWindowSize3() {
326361
326369
  return getWindowSize2(`/session/${state.appiumSessionId}`);
326362
326370
  }
326363
326371
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
326364
- function killPort2(port) {
326365
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return;
326372
+ var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
326373
+ var BRIDGE_PID_FILE = (0, import_path2.join)((0, import_os3.homedir)(), ".validate.qa", "mobile-bridge.pid");
326374
+ function readBridgePidFile() {
326366
326375
  try {
326367
- const pids = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${port}`], { encoding: "utf-8" }).trim();
326368
- if (pids) {
326369
- const pidList = pids.split("\n").filter(Boolean);
326370
- for (const pid of pidList) {
326371
- if (!/^\d+$/.test(pid)) continue;
326372
- let command = "";
326373
- try {
326374
- command = (0, import_child_process3.execFileSync)("ps", ["-p", pid, "-o", "command="], { encoding: "utf-8" }).trim();
326375
- } catch {
326376
- continue;
326377
- }
326378
- if (!command.includes(BRIDGE_PROCESS_MARKER)) {
326379
- continue;
326380
- }
326381
- try {
326382
- (0, import_child_process3.execFileSync)("kill", ["-9", pid], { stdio: "ignore" });
326383
- } catch {
326384
- }
326385
- }
326376
+ const raw = (0, import_fs2.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
326377
+ const pid = parseInt(raw, 10);
326378
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
326379
+ } catch {
326380
+ return null;
326381
+ }
326382
+ }
326383
+ function writeBridgePidFile(pid) {
326384
+ try {
326385
+ (0, import_fs2.mkdirSync)((0, import_path2.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
326386
+ (0, import_fs2.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
326387
+ } catch {
326388
+ }
326389
+ }
326390
+ function clearBridgePidFile() {
326391
+ try {
326392
+ (0, import_fs2.unlinkSync)(BRIDGE_PID_FILE);
326393
+ } catch {
326394
+ }
326395
+ }
326396
+ function killStaleBridge() {
326397
+ const pid = readBridgePidFile();
326398
+ if (!pid) return;
326399
+ try {
326400
+ (0, import_child_process3.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
326401
+ } catch {
326402
+ clearBridgePidFile();
326403
+ return;
326404
+ }
326405
+ try {
326406
+ const pidsOnPort = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
326407
+ const pidList = pidsOnPort.split("\n").filter(Boolean);
326408
+ if (!pidList.includes(String(pid))) {
326409
+ return;
326386
326410
  }
326411
+ (0, import_child_process3.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
326412
+ (0, import_child_process3.execFileSync)("sleep", ["1"], { stdio: "ignore" });
326387
326413
  } catch {
326414
+ } finally {
326415
+ clearBridgePidFile();
326388
326416
  }
326389
326417
  }
326390
326418
  function parseBody(req) {
@@ -327009,7 +327037,7 @@ async function startMobileBridge(options) {
327009
327037
  if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
327010
327038
  process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
327011
327039
  }
327012
- killPort2(port);
327040
+ killStaleBridge();
327013
327041
  return new Promise((resolve, reject) => {
327014
327042
  server2 = (0, import_http.createServer)(route);
327015
327043
  server2.on("error", (err) => {
@@ -327025,6 +327053,7 @@ async function startMobileBridge(options) {
327025
327053
  state.port = port;
327026
327054
  state.tunnelToken = tunnelToken;
327027
327055
  state.tunnelUrl = void 0;
327056
+ writeBridgePidFile(process.pid);
327028
327057
  let tunnelUrl;
327029
327058
  if (enableTunnel) {
327030
327059
  try {
@@ -327092,6 +327121,7 @@ async function startMobileBridge(options) {
327092
327121
  let checkInterval = null;
327093
327122
  const timeout = setTimeout(() => {
327094
327123
  if (checkInterval) clearInterval(checkInterval);
327124
+ checkInterval = null;
327095
327125
  if (!tunnelUrl) {
327096
327126
  reject2(new Error("ngrok tunnel failed to start"));
327097
327127
  }
@@ -327100,6 +327130,7 @@ async function startMobileBridge(options) {
327100
327130
  if (tunnelUrl) {
327101
327131
  clearTimeout(timeout);
327102
327132
  if (checkInterval) clearInterval(checkInterval);
327133
+ checkInterval = null;
327103
327134
  resolve2();
327104
327135
  }
327105
327136
  }, 500);
@@ -327129,6 +327160,7 @@ async function startMobileBridge(options) {
327129
327160
  async function stopMobileBridge() {
327130
327161
  stopAllMirrorClients();
327131
327162
  stopSessionReaper();
327163
+ clearBridgePidFile();
327132
327164
  if (state.appiumSessionId) {
327133
327165
  try {
327134
327166
  validateSessionId(state.appiumSessionId);
@@ -327989,6 +328021,219 @@ async function startMobileNetworkCapture(options) {
327989
328021
  };
327990
328022
  }
327991
328023
 
328024
+ // src/services/doctor.ts
328025
+ var import_child_process4 = require("child_process");
328026
+ var import_fs3 = require("fs");
328027
+ var import_os4 = require("os");
328028
+ var C = {
328029
+ reset: "\x1B[0m",
328030
+ bold: "\x1B[1m",
328031
+ green: "\x1B[32m",
328032
+ red: "\x1B[31m",
328033
+ yellow: "\x1B[33m",
328034
+ cyan: "\x1B[36m",
328035
+ gray: "\x1B[90m"
328036
+ };
328037
+ function run(cmd) {
328038
+ try {
328039
+ return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
328040
+ } catch {
328041
+ return null;
328042
+ }
328043
+ }
328044
+ function check(name, fn) {
328045
+ try {
328046
+ const result = fn();
328047
+ return { name, ...result };
328048
+ } catch (err) {
328049
+ return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
328050
+ }
328051
+ }
328052
+ function checkWeb() {
328053
+ const results = [];
328054
+ results.push(check("Node.js", () => {
328055
+ const version = process.version;
328056
+ const major = parseInt(version.slice(1).split(".")[0], 10);
328057
+ return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
328058
+ }));
328059
+ results.push(check("Playwright", () => {
328060
+ const version = run("npx playwright --version");
328061
+ if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
328062
+ return { ok: true, detail: version };
328063
+ }));
328064
+ return results;
328065
+ }
328066
+ function checkIOS() {
328067
+ const results = [];
328068
+ const isMac = (0, import_os4.platform)() === "darwin";
328069
+ results.push(check("macOS", () => ({
328070
+ ok: isMac,
328071
+ detail: isMac ? `${(0, import_os4.platform)()} detected` : "iOS testing requires macOS"
328072
+ })));
328073
+ if (!isMac) return results;
328074
+ results.push(check("Xcode", () => {
328075
+ const version = run("xcodebuild -version");
328076
+ if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
328077
+ const firstLine = version.split("\n")[0];
328078
+ return { ok: true, detail: firstLine };
328079
+ }));
328080
+ results.push(check("Xcode Command Line Tools", () => {
328081
+ const path = run("xcode-select -p");
328082
+ return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
328083
+ }));
328084
+ results.push(check("Appium xcuitest driver", () => {
328085
+ const drivers = run("appium driver list --installed --json");
328086
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328087
+ try {
328088
+ const parsed = JSON.parse(drivers);
328089
+ const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
328090
+ return {
328091
+ ok: !!hasXcuitest,
328092
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328093
+ };
328094
+ } catch {
328095
+ const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
328096
+ return {
328097
+ ok: hasXcuitest,
328098
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328099
+ };
328100
+ }
328101
+ }));
328102
+ results.push(check("iOS Devices", () => {
328103
+ const json = run("xcrun simctl list devices booted --json");
328104
+ let bootedSimulators = 0;
328105
+ if (json) {
328106
+ try {
328107
+ const parsed = JSON.parse(json);
328108
+ const devices = Object.values(parsed.devices ?? {}).flat();
328109
+ bootedSimulators = devices.filter((d) => d.state === "Booted").length;
328110
+ } catch {
328111
+ }
328112
+ }
328113
+ const realDevices = discoverRealIOSDevices().length;
328114
+ const total = bootedSimulators + realDevices;
328115
+ if (total === 0) {
328116
+ return {
328117
+ ok: false,
328118
+ detail: "No devices found. Boot a simulator or connect a real device via USB."
328119
+ };
328120
+ }
328121
+ const details = [];
328122
+ if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
328123
+ if (realDevices > 0) details.push(`${realDevices} real device(s)`);
328124
+ return {
328125
+ ok: true,
328126
+ detail: `${total} device(s) available: ${details.join(", ")}`
328127
+ };
328128
+ }));
328129
+ return results;
328130
+ }
328131
+ function checkAndroid() {
328132
+ const results = [];
328133
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
328134
+ results.push(check("ANDROID_HOME", () => ({
328135
+ ok: !!androidHome && (0, import_fs3.existsSync)(androidHome),
328136
+ detail: androidHome ? (0, import_fs3.existsSync)(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
328137
+ })));
328138
+ if (!androidHome || !(0, import_fs3.existsSync)(androidHome)) return results;
328139
+ results.push(check("adb", () => {
328140
+ const version = run("adb version");
328141
+ if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
328142
+ const firstLine = version.split("\n")[0];
328143
+ return { ok: true, detail: firstLine };
328144
+ }));
328145
+ results.push(check("Appium uiautomator2 driver", () => {
328146
+ const drivers = run("appium driver list --installed --json");
328147
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328148
+ try {
328149
+ const parsed = JSON.parse(drivers);
328150
+ const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
328151
+ return {
328152
+ ok: !!hasUia2,
328153
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328154
+ };
328155
+ } catch {
328156
+ const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
328157
+ return {
328158
+ ok: hasUia2,
328159
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328160
+ };
328161
+ }
328162
+ }));
328163
+ results.push(check("Android Devices", () => {
328164
+ const output = run("adb devices");
328165
+ if (!output) return { ok: false, detail: "adb devices failed" };
328166
+ const lines = output.split("\n").filter((l) => l.includes(" device"));
328167
+ const emulators = lines.filter((l) => l.startsWith("emulator-"));
328168
+ const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
328169
+ const total = emulators.length + realDevices.length;
328170
+ if (total === 0) {
328171
+ return {
328172
+ ok: false,
328173
+ detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
328174
+ };
328175
+ }
328176
+ const details = [];
328177
+ if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
328178
+ if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
328179
+ return {
328180
+ ok: true,
328181
+ detail: `${total} device(s) available: ${details.join(", ")}`
328182
+ };
328183
+ }));
328184
+ return results;
328185
+ }
328186
+ function checkTunnel() {
328187
+ const version = run("ngrok version") || run("ngrok --version");
328188
+ if (!version) {
328189
+ return [{
328190
+ name: "ngrok",
328191
+ ok: false,
328192
+ detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
328193
+ }];
328194
+ }
328195
+ const match = version.match(/(\d+)\.\d+\.\d+/);
328196
+ const major = match ? parseInt(match[1], 10) : 0;
328197
+ return [{
328198
+ name: "ngrok",
328199
+ ok: major >= 3,
328200
+ detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
328201
+ }];
328202
+ }
328203
+ function runDoctor() {
328204
+ const web = checkWeb();
328205
+ const ios = checkIOS();
328206
+ const android = checkAndroid();
328207
+ const tunnel = checkTunnel();
328208
+ const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
328209
+ return { web, ios, android, tunnel, allPassed };
328210
+ }
328211
+ function printDoctorReport(report) {
328212
+ console.log("");
328213
+ console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
328214
+ console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
328215
+ const printSection = (title, checks) => {
328216
+ console.log("");
328217
+ console.log(` ${C.bold}${title}:${C.reset}`);
328218
+ for (const c of checks) {
328219
+ const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
328220
+ console.log(` ${icon} ${c.name}: ${c.detail}`);
328221
+ }
328222
+ };
328223
+ printSection("Web Testing", report.web);
328224
+ printSection("iOS Testing", report.ios);
328225
+ printSection("Android Testing", report.android);
328226
+ printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
328227
+ console.log("");
328228
+ if (report.allPassed) {
328229
+ console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
328230
+ } else {
328231
+ const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
328232
+ console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
328233
+ }
328234
+ console.log("");
328235
+ }
328236
+
327992
328237
  // src/runner.ts
327993
328238
  init_dist();
327994
328239
  var import_runner_core11 = __toESM(require_dist2());
@@ -328078,7 +328323,7 @@ var browserPool = new import_runner_core6.BrowserPool({
328078
328323
  maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
328079
328324
  retireAfterContexts: 30
328080
328325
  });
328081
- var C = {
328326
+ var C2 = {
328082
328327
  reset: "\x1B[0m",
328083
328328
  bold: "\x1B[1m",
328084
328329
  green: "\x1B[32m",
@@ -328088,25 +328333,25 @@ var C = {
328088
328333
  gray: "\x1B[90m"
328089
328334
  };
328090
328335
  var log = {
328091
- info: (msg) => console.log(`${C.cyan}\u2139${C.reset} ${msg}`),
328092
- ok: (msg) => console.log(`${C.green}\u2713${C.reset} ${msg}`),
328093
- fail: (msg) => console.log(`${C.red}\u2717${C.reset} ${msg}`),
328094
- warn: (msg) => console.log(`${C.yellow}\u26A0${C.reset} ${msg}`),
328095
- dim: (msg) => console.log(`${C.gray}${msg}${C.reset}`)
328336
+ info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
328337
+ ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
328338
+ fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
328339
+ warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
328340
+ dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
328096
328341
  };
328097
328342
  var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
328098
328343
  function getRunsDir() {
328099
- const dir = process.env.RUNS_DIR || (0, import_path2.join)((0, import_os3.homedir)(), ".validate.qa", "runs");
328100
- if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
328344
+ const dir = process.env.RUNS_DIR || (0, import_path3.join)((0, import_os5.homedir)(), ".validate.qa", "runs");
328345
+ if (!(0, import_fs4.existsSync)(dir)) (0, import_fs4.mkdirSync)(dir, { recursive: true });
328101
328346
  return dir;
328102
328347
  }
328103
328348
  function authStatePath(projectId) {
328104
- return (0, import_path2.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328349
+ return (0, import_path3.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328105
328350
  }
328106
328351
  function getValidAuthState(projectId) {
328107
328352
  const path = authStatePath(projectId);
328108
- if (!(0, import_fs2.existsSync)(path)) return void 0;
328109
- const age = Date.now() - (0, import_fs2.statSync)(path).mtimeMs;
328353
+ if (!(0, import_fs4.existsSync)(path)) return void 0;
328354
+ const age = Date.now() - (0, import_fs4.statSync)(path).mtimeMs;
328110
328355
  return age < AUTH_MAX_AGE_MS ? path : void 0;
328111
328356
  }
328112
328357
  function buildHeaders(authToken, projectId) {
@@ -329048,9 +329293,9 @@ ${finalVerifyResult.logs ?? ""}`;
329048
329293
  (l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
329049
329294
  );
329050
329295
  if (phaseLines.length > 0) {
329051
- console.log(`${C.gray}\u2500\u2500 Heal summary \u2500\u2500${C.reset}`);
329296
+ console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
329052
329297
  for (const line of phaseLines) console.log(line);
329053
- console.log(`${C.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C.reset}`);
329298
+ console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
329054
329299
  }
329055
329300
  }
329056
329301
  log[result2.passed ? "ok" : "fail"](
@@ -329589,11 +329834,18 @@ ${finalVerifyResult.logs ?? ""}`;
329589
329834
  }
329590
329835
  try {
329591
329836
  if (isNativeMobile) {
329592
- const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
329593
329837
  const mobileTarget = ctx.target;
329594
329838
  if (!mobileTarget || !mobileTarget.appId) {
329595
329839
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
329596
329840
  }
329841
+ const targetPlatform = mobileTarget.platform;
329842
+ const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
329843
+ if (!platform3) {
329844
+ throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
329845
+ }
329846
+ if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
329847
+ throw new Error("Native mobile discovery medium does not match target.platform.");
329848
+ }
329597
329849
  if (getBridgeState().hasActiveSession) {
329598
329850
  throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
329599
329851
  }
@@ -330022,9 +330274,9 @@ ${finalVerifyResult.logs ?? ""}`;
330022
330274
  } else {
330023
330275
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
330024
330276
  if (result.logs) {
330025
- console.log(`${C.gray}\u2500\u2500 Runner logs \u2500\u2500${C.reset}`);
330277
+ console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
330026
330278
  console.log(result.logs);
330027
- console.log(`${C.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C.reset}`);
330279
+ console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
330028
330280
  }
330029
330281
  }
330030
330282
  } catch (err) {
@@ -330046,20 +330298,24 @@ ${finalVerifyResult.logs ?? ""}`;
330046
330298
  }
330047
330299
  async function startRunner(opts) {
330048
330300
  const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
330301
+ process.env.SERVER_URL = serverUrl;
330302
+ process.env.AUTH_TOKEN = authToken;
330303
+ process.env.RUNNER_TOKEN = authToken;
330304
+ if (projectId) process.env.PROJECT_ID = projectId;
330049
330305
  const headers = buildHeaders(authToken, projectId);
330050
330306
  console.log("");
330051
- console.log(`${C.bold}${C.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C.reset}`);
330052
- console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
330307
+ console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
330308
+ console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330053
330309
  log.info(`Server: ${serverUrl}`);
330054
330310
  log.info(`Project: ${projectId}`);
330055
330311
  log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
330056
330312
  log.info(`Interval: ${pollIntervalMs}ms`);
330057
330313
  if (process.env.DISCOVERY_PROMPT_V3 === "true") {
330058
- log.ok("Discovery: V3 forced (DISCOVERY_PROMPT_V3=true)");
330314
+ log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
330059
330315
  } else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
330060
- log.ok("Discovery: V2.5 forced (DISCOVERY_PROMPT_V3=false)");
330316
+ log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
330061
330317
  } else {
330062
- log.ok("Discovery: auto mode (Grok -> V2.5, others -> V3)");
330318
+ log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
330063
330319
  }
330064
330320
  if ((0, import_runner_core5.hasUsableChromeProfile)()) {
330065
330321
  log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
@@ -330071,8 +330327,8 @@ async function startRunner(opts) {
330071
330327
  const devices = discoverAllDevices();
330072
330328
  const capabilities = {
330073
330329
  web: true,
330074
- ios: devices.some((d) => d.platform === "IOS"),
330075
- android: devices.some((d) => d.platform === "ANDROID")
330330
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330331
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330076
330332
  };
330077
330333
  log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
330078
330334
  try {
@@ -330133,6 +330389,33 @@ async function startRunner(opts) {
330133
330389
  const startedAt = Date.now();
330134
330390
  const MAX_BACKOFF_MS = 6e4;
330135
330391
  let consecutivePollErrors = 0;
330392
+ let cachedDoctor = null;
330393
+ const DOCTOR_TTL_MS = 5 * 60 * 1e3;
330394
+ let doctorRefreshInFlight = false;
330395
+ const refreshDoctorInBackground = () => {
330396
+ if (doctorRefreshInFlight) return;
330397
+ doctorRefreshInFlight = true;
330398
+ setImmediate(() => {
330399
+ try {
330400
+ const report = runDoctor();
330401
+ cachedDoctor = { report, at: Date.now() };
330402
+ } catch (err) {
330403
+ log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
330404
+ } finally {
330405
+ doctorRefreshInFlight = false;
330406
+ }
330407
+ });
330408
+ };
330409
+ const getCachedDoctor = () => {
330410
+ if (!enableMobile) return void 0;
330411
+ if (!cachedDoctor) {
330412
+ refreshDoctorInBackground();
330413
+ return void 0;
330414
+ }
330415
+ const now = Date.now();
330416
+ if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
330417
+ return cachedDoctor.report;
330418
+ };
330136
330419
  log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
330137
330420
  const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
330138
330421
  if (poolSize > 0) {
@@ -330241,7 +330524,11 @@ async function startRunner(opts) {
330241
330524
  const sendHeartbeat = async () => {
330242
330525
  try {
330243
330526
  const devices = enableMobile ? discoverAllDevices() : [];
330244
- const capabilities = enableMobile ? { web: true, ios: devices.some((d) => d.platform === "IOS"), android: devices.some((d) => d.platform === "ANDROID") } : { web: true, ios: false, android: false };
330527
+ const capabilities = enableMobile ? {
330528
+ web: true,
330529
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330530
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330531
+ } : { web: true, ios: false, android: false };
330245
330532
  const bridge = enableMobile ? getBridgeState() : void 0;
330246
330533
  const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
330247
330534
  const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
@@ -330272,7 +330559,10 @@ async function startRunner(opts) {
330272
330559
  resourceGovernor: resourceStatus.governor,
330273
330560
  processInventory,
330274
330561
  aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
330275
- liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
330562
+ liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
330563
+ // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
330564
+ // Cached & cheap — see getCachedDoctor above.
330565
+ doctor: getCachedDoctor()
330276
330566
  }),
330277
330567
  signal: AbortSignal.timeout(1e4)
330278
330568
  });
@@ -330472,219 +330762,6 @@ async function startRunner(opts) {
330472
330762
  }
330473
330763
  }
330474
330764
 
330475
- // src/services/doctor.ts
330476
- var import_child_process4 = require("child_process");
330477
- var import_fs3 = require("fs");
330478
- var import_os4 = require("os");
330479
- var C2 = {
330480
- reset: "\x1B[0m",
330481
- bold: "\x1B[1m",
330482
- green: "\x1B[32m",
330483
- red: "\x1B[31m",
330484
- yellow: "\x1B[33m",
330485
- cyan: "\x1B[36m",
330486
- gray: "\x1B[90m"
330487
- };
330488
- function run(cmd) {
330489
- try {
330490
- return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
330491
- } catch {
330492
- return null;
330493
- }
330494
- }
330495
- function check(name, fn) {
330496
- try {
330497
- const result = fn();
330498
- return { name, ...result };
330499
- } catch (err) {
330500
- return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
330501
- }
330502
- }
330503
- function checkWeb() {
330504
- const results = [];
330505
- results.push(check("Node.js", () => {
330506
- const version = process.version;
330507
- const major = parseInt(version.slice(1).split(".")[0], 10);
330508
- return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
330509
- }));
330510
- results.push(check("Playwright", () => {
330511
- const version = run("npx playwright --version");
330512
- if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
330513
- return { ok: true, detail: version };
330514
- }));
330515
- return results;
330516
- }
330517
- function checkIOS() {
330518
- const results = [];
330519
- const isMac = (0, import_os4.platform)() === "darwin";
330520
- results.push(check("macOS", () => ({
330521
- ok: isMac,
330522
- detail: isMac ? `${(0, import_os4.platform)()} detected` : "iOS testing requires macOS"
330523
- })));
330524
- if (!isMac) return results;
330525
- results.push(check("Xcode", () => {
330526
- const version = run("xcodebuild -version");
330527
- if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
330528
- const firstLine = version.split("\n")[0];
330529
- return { ok: true, detail: firstLine };
330530
- }));
330531
- results.push(check("Xcode Command Line Tools", () => {
330532
- const path = run("xcode-select -p");
330533
- return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
330534
- }));
330535
- results.push(check("Appium xcuitest driver", () => {
330536
- const drivers = run("appium driver list --installed --json");
330537
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330538
- try {
330539
- const parsed = JSON.parse(drivers);
330540
- const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
330541
- return {
330542
- ok: !!hasXcuitest,
330543
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330544
- };
330545
- } catch {
330546
- const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
330547
- return {
330548
- ok: hasXcuitest,
330549
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330550
- };
330551
- }
330552
- }));
330553
- results.push(check("iOS Devices", () => {
330554
- const json = run("xcrun simctl list devices booted --json");
330555
- let bootedSimulators = 0;
330556
- if (json) {
330557
- try {
330558
- const parsed = JSON.parse(json);
330559
- const devices = Object.values(parsed.devices ?? {}).flat();
330560
- bootedSimulators = devices.filter((d) => d.state === "Booted").length;
330561
- } catch {
330562
- }
330563
- }
330564
- const realDevices = discoverRealIOSDevices().length;
330565
- const total = bootedSimulators + realDevices;
330566
- if (total === 0) {
330567
- return {
330568
- ok: false,
330569
- detail: "No devices found. Boot a simulator or connect a real device via USB."
330570
- };
330571
- }
330572
- const details = [];
330573
- if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
330574
- if (realDevices > 0) details.push(`${realDevices} real device(s)`);
330575
- return {
330576
- ok: true,
330577
- detail: `${total} device(s) available: ${details.join(", ")}`
330578
- };
330579
- }));
330580
- return results;
330581
- }
330582
- function checkAndroid() {
330583
- const results = [];
330584
- const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
330585
- results.push(check("ANDROID_HOME", () => ({
330586
- ok: !!androidHome && (0, import_fs3.existsSync)(androidHome),
330587
- detail: androidHome ? (0, import_fs3.existsSync)(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
330588
- })));
330589
- if (!androidHome || !(0, import_fs3.existsSync)(androidHome)) return results;
330590
- results.push(check("adb", () => {
330591
- const version = run("adb version");
330592
- if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
330593
- const firstLine = version.split("\n")[0];
330594
- return { ok: true, detail: firstLine };
330595
- }));
330596
- results.push(check("Appium uiautomator2 driver", () => {
330597
- const drivers = run("appium driver list --installed --json");
330598
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330599
- try {
330600
- const parsed = JSON.parse(drivers);
330601
- const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
330602
- return {
330603
- ok: !!hasUia2,
330604
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330605
- };
330606
- } catch {
330607
- const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
330608
- return {
330609
- ok: hasUia2,
330610
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330611
- };
330612
- }
330613
- }));
330614
- results.push(check("Android Devices", () => {
330615
- const output = run("adb devices");
330616
- if (!output) return { ok: false, detail: "adb devices failed" };
330617
- const lines = output.split("\n").filter((l) => l.includes(" device"));
330618
- const emulators = lines.filter((l) => l.startsWith("emulator-"));
330619
- const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
330620
- const total = emulators.length + realDevices.length;
330621
- if (total === 0) {
330622
- return {
330623
- ok: false,
330624
- detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
330625
- };
330626
- }
330627
- const details = [];
330628
- if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
330629
- if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
330630
- return {
330631
- ok: true,
330632
- detail: `${total} device(s) available: ${details.join(", ")}`
330633
- };
330634
- }));
330635
- return results;
330636
- }
330637
- function checkTunnel() {
330638
- const version = run("ngrok version") || run("ngrok --version");
330639
- if (!version) {
330640
- return [{
330641
- name: "ngrok",
330642
- ok: false,
330643
- detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
330644
- }];
330645
- }
330646
- const match = version.match(/(\d+)\.\d+\.\d+/);
330647
- const major = match ? parseInt(match[1], 10) : 0;
330648
- return [{
330649
- name: "ngrok",
330650
- ok: major >= 3,
330651
- detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
330652
- }];
330653
- }
330654
- function runDoctor() {
330655
- const web = checkWeb();
330656
- const ios = checkIOS();
330657
- const android = checkAndroid();
330658
- const tunnel = checkTunnel();
330659
- const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
330660
- return { web, ios, android, tunnel, allPassed };
330661
- }
330662
- function printDoctorReport(report) {
330663
- console.log("");
330664
- console.log(`${C2.bold}${C2.cyan}\u{1F3E5} validate.qa Doctor${C2.reset}`);
330665
- console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330666
- const printSection = (title, checks) => {
330667
- console.log("");
330668
- console.log(` ${C2.bold}${title}:${C2.reset}`);
330669
- for (const c of checks) {
330670
- const icon = c.ok ? `${C2.green}\u2713${C2.reset}` : `${C2.red}\u2717${C2.reset}`;
330671
- console.log(` ${icon} ${c.name}: ${c.detail}`);
330672
- }
330673
- };
330674
- printSection("Web Testing", report.web);
330675
- printSection("iOS Testing", report.ios);
330676
- printSection("Android Testing", report.android);
330677
- printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
330678
- console.log("");
330679
- if (report.allPassed) {
330680
- console.log(` ${C2.green}\u2705 All checks passed - ready for web + mobile testing${C2.reset}`);
330681
- } else {
330682
- const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
330683
- console.log(` ${C2.yellow}\u26A0 ${failCount} check(s) need attention${C2.reset}`);
330684
- }
330685
- console.log("");
330686
- }
330687
-
330688
330765
  // src/cli.ts
330689
330766
  var pkg = require_package4();
330690
330767
  var program = new import_commander.Command();