@validate.qa/runner 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +450 -334
- package/dist/cli.mjs +455 -339
- 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
|
|
1408
|
+
return `id=${value}`;
|
|
1409
1409
|
return `~${value}`;
|
|
1410
1410
|
}
|
|
1411
1411
|
function selectorCandidatesForStep(step) {
|
|
@@ -7552,7 +7552,7 @@ Each entry is exactly one of:
|
|
|
7552
7552
|
const systemPrompt = context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports2.SCENARIO_SYSTEM_PROMPT;
|
|
7553
7553
|
const userPrompt = formatEvidenceForPlanner(compressed, evidence, context);
|
|
7554
7554
|
logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
|
|
7555
|
-
const aiClient = options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)();
|
|
7555
|
+
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)());
|
|
7556
7556
|
const messages = [
|
|
7557
7557
|
{ role: "system", content: systemPrompt },
|
|
7558
7558
|
{ role: "user", content: userPrompt }
|
|
@@ -7764,6 +7764,7 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
7764
7764
|
exports2.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
|
|
7765
7765
|
exports2.buildMobileSiteMap = buildMobileSiteMap;
|
|
7766
7766
|
exports2.runMobileAIPlanner = runMobileAIPlanner;
|
|
7767
|
+
var ai_provider_js_1 = require_ai_provider();
|
|
7767
7768
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
7768
7769
|
exports2.MOBILE_SURVEY_SYSTEM_PROMPT = `You are a feature analyst for a native mobile application (iOS or Android). You receive evidence from an AI explorer that mapped the app's screens by tapping, typing, and swiping through the live UI. Your job is to organize screens into meaningful feature groups.
|
|
7769
7770
|
|
|
@@ -8163,6 +8164,8 @@ ${constraints.join("\n")}`);
|
|
|
8163
8164
|
}
|
|
8164
8165
|
async function runMobileAIPlanner(input, options = {}) {
|
|
8165
8166
|
const planner = options.planner ?? discover_ai_planner_js_1.runAIPlanner;
|
|
8167
|
+
const shouldBuildMobileAIClient = (process.env.SERVER_URL ?? "").trim().length > 0 || !options.planner;
|
|
8168
|
+
const aiClient = options.aiClient ?? (shouldBuildMobileAIClient ? options.modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getMobileAIClient)() : void 0);
|
|
8166
8169
|
const siteMap = buildMobileSiteMap(input.graph, {
|
|
8167
8170
|
projectId: input.projectId,
|
|
8168
8171
|
appId: input.appId
|
|
@@ -8188,7 +8191,8 @@ ${constraints.join("\n")}`);
|
|
|
8188
8191
|
testBudget: input.testBudget
|
|
8189
8192
|
}, {
|
|
8190
8193
|
modelOverride: options.modelOverride,
|
|
8191
|
-
onAICall: options.onAICall
|
|
8194
|
+
onAICall: options.onAICall,
|
|
8195
|
+
...aiClient ? { aiClient } : {}
|
|
8192
8196
|
});
|
|
8193
8197
|
}
|
|
8194
8198
|
}
|
|
@@ -21738,7 +21742,7 @@ var require_util = __commonJS({
|
|
|
21738
21742
|
return path;
|
|
21739
21743
|
}
|
|
21740
21744
|
exports2.normalize = normalize;
|
|
21741
|
-
function
|
|
21745
|
+
function join5(aRoot, aPath) {
|
|
21742
21746
|
if (aRoot === "") {
|
|
21743
21747
|
aRoot = ".";
|
|
21744
21748
|
}
|
|
@@ -21770,7 +21774,7 @@ var require_util = __commonJS({
|
|
|
21770
21774
|
}
|
|
21771
21775
|
return joined;
|
|
21772
21776
|
}
|
|
21773
|
-
exports2.join =
|
|
21777
|
+
exports2.join = join5;
|
|
21774
21778
|
exports2.isAbsolute = function(aPath) {
|
|
21775
21779
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
21776
21780
|
};
|
|
@@ -21943,7 +21947,7 @@ var require_util = __commonJS({
|
|
|
21943
21947
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
21944
21948
|
}
|
|
21945
21949
|
}
|
|
21946
|
-
sourceURL =
|
|
21950
|
+
sourceURL = join5(urlGenerate(parsed), sourceURL);
|
|
21947
21951
|
}
|
|
21948
21952
|
return normalize(sourceURL);
|
|
21949
21953
|
}
|
|
@@ -243098,7 +243102,7 @@ var require_thread_stream = __commonJS({
|
|
|
243098
243102
|
var { version } = require_package();
|
|
243099
243103
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
243100
243104
|
var { Worker } = require("worker_threads");
|
|
243101
|
-
var { join:
|
|
243105
|
+
var { join: join5 } = require("path");
|
|
243102
243106
|
var { pathToFileURL } = require("url");
|
|
243103
243107
|
var { wait } = require_wait();
|
|
243104
243108
|
var {
|
|
@@ -243149,7 +243153,7 @@ var require_thread_stream = __commonJS({
|
|
|
243149
243153
|
function createWorker(stream, opts) {
|
|
243150
243154
|
const { filename, workerData } = opts;
|
|
243151
243155
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
243152
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
243156
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
|
|
243153
243157
|
const worker = new Worker(toExecute, {
|
|
243154
243158
|
...opts.workerOpts,
|
|
243155
243159
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -243617,7 +243621,7 @@ var require_transport = __commonJS({
|
|
|
243617
243621
|
var { createRequire } = require("module");
|
|
243618
243622
|
var { existsSync: existsSync4 } = require("fs");
|
|
243619
243623
|
var getCallers = require_caller();
|
|
243620
|
-
var { join:
|
|
243624
|
+
var { join: join5, isAbsolute, sep } = require("path");
|
|
243621
243625
|
var { fileURLToPath } = require("url");
|
|
243622
243626
|
var sleep = require_atomic_sleep();
|
|
243623
243627
|
var onExit = require_on_exit_leak_free();
|
|
@@ -243770,7 +243774,7 @@ var require_transport = __commonJS({
|
|
|
243770
243774
|
throw new Error("only one of target or targets can be specified");
|
|
243771
243775
|
}
|
|
243772
243776
|
if (targets) {
|
|
243773
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
243777
|
+
target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
|
|
243774
243778
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
243775
243779
|
return {
|
|
243776
243780
|
...dest,
|
|
@@ -243788,7 +243792,7 @@ var require_transport = __commonJS({
|
|
|
243788
243792
|
});
|
|
243789
243793
|
});
|
|
243790
243794
|
} else if (pipeline) {
|
|
243791
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
243795
|
+
target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
|
|
243792
243796
|
options.pipelines = [pipeline.map((dest) => {
|
|
243793
243797
|
return {
|
|
243794
243798
|
...dest,
|
|
@@ -243811,7 +243815,7 @@ var require_transport = __commonJS({
|
|
|
243811
243815
|
return origin;
|
|
243812
243816
|
}
|
|
243813
243817
|
if (origin === "pino/file") {
|
|
243814
|
-
return
|
|
243818
|
+
return join5(__dirname, "..", "file.js");
|
|
243815
243819
|
}
|
|
243816
243820
|
let fixTarget2;
|
|
243817
243821
|
for (const filePath of callers) {
|
|
@@ -244792,7 +244796,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
244792
244796
|
return circularValue;
|
|
244793
244797
|
}
|
|
244794
244798
|
let res = "";
|
|
244795
|
-
let
|
|
244799
|
+
let join5 = ",";
|
|
244796
244800
|
const originalIndentation = indentation;
|
|
244797
244801
|
if (Array.isArray(value)) {
|
|
244798
244802
|
if (value.length === 0) {
|
|
@@ -244806,7 +244810,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
244806
244810
|
indentation += spacer;
|
|
244807
244811
|
res += `
|
|
244808
244812
|
${indentation}`;
|
|
244809
|
-
|
|
244813
|
+
join5 = `,
|
|
244810
244814
|
${indentation}`;
|
|
244811
244815
|
}
|
|
244812
244816
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -244814,13 +244818,13 @@ ${indentation}`;
|
|
|
244814
244818
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
244815
244819
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
244816
244820
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
244817
|
-
res +=
|
|
244821
|
+
res += join5;
|
|
244818
244822
|
}
|
|
244819
244823
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
244820
244824
|
res += tmp !== void 0 ? tmp : "null";
|
|
244821
244825
|
if (value.length - 1 > maximumBreadth) {
|
|
244822
244826
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
244823
|
-
res += `${
|
|
244827
|
+
res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
244824
244828
|
}
|
|
244825
244829
|
if (spacer !== "") {
|
|
244826
244830
|
res += `
|
|
@@ -244841,7 +244845,7 @@ ${originalIndentation}`;
|
|
|
244841
244845
|
let separator = "";
|
|
244842
244846
|
if (spacer !== "") {
|
|
244843
244847
|
indentation += spacer;
|
|
244844
|
-
|
|
244848
|
+
join5 = `,
|
|
244845
244849
|
${indentation}`;
|
|
244846
244850
|
whitespace = " ";
|
|
244847
244851
|
}
|
|
@@ -244855,13 +244859,13 @@ ${indentation}`;
|
|
|
244855
244859
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
244856
244860
|
if (tmp !== void 0) {
|
|
244857
244861
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
244858
|
-
separator =
|
|
244862
|
+
separator = join5;
|
|
244859
244863
|
}
|
|
244860
244864
|
}
|
|
244861
244865
|
if (keyLength > maximumBreadth) {
|
|
244862
244866
|
const removedKeys = keyLength - maximumBreadth;
|
|
244863
244867
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
244864
|
-
separator =
|
|
244868
|
+
separator = join5;
|
|
244865
244869
|
}
|
|
244866
244870
|
if (spacer !== "" && separator.length > 1) {
|
|
244867
244871
|
res = `
|
|
@@ -244902,7 +244906,7 @@ ${originalIndentation}`;
|
|
|
244902
244906
|
}
|
|
244903
244907
|
const originalIndentation = indentation;
|
|
244904
244908
|
let res = "";
|
|
244905
|
-
let
|
|
244909
|
+
let join5 = ",";
|
|
244906
244910
|
if (Array.isArray(value)) {
|
|
244907
244911
|
if (value.length === 0) {
|
|
244908
244912
|
return "[]";
|
|
@@ -244915,7 +244919,7 @@ ${originalIndentation}`;
|
|
|
244915
244919
|
indentation += spacer;
|
|
244916
244920
|
res += `
|
|
244917
244921
|
${indentation}`;
|
|
244918
|
-
|
|
244922
|
+
join5 = `,
|
|
244919
244923
|
${indentation}`;
|
|
244920
244924
|
}
|
|
244921
244925
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -244923,13 +244927,13 @@ ${indentation}`;
|
|
|
244923
244927
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
244924
244928
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
244925
244929
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
244926
|
-
res +=
|
|
244930
|
+
res += join5;
|
|
244927
244931
|
}
|
|
244928
244932
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
244929
244933
|
res += tmp !== void 0 ? tmp : "null";
|
|
244930
244934
|
if (value.length - 1 > maximumBreadth) {
|
|
244931
244935
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
244932
|
-
res += `${
|
|
244936
|
+
res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
244933
244937
|
}
|
|
244934
244938
|
if (spacer !== "") {
|
|
244935
244939
|
res += `
|
|
@@ -244942,7 +244946,7 @@ ${originalIndentation}`;
|
|
|
244942
244946
|
let whitespace = "";
|
|
244943
244947
|
if (spacer !== "") {
|
|
244944
244948
|
indentation += spacer;
|
|
244945
|
-
|
|
244949
|
+
join5 = `,
|
|
244946
244950
|
${indentation}`;
|
|
244947
244951
|
whitespace = " ";
|
|
244948
244952
|
}
|
|
@@ -244951,7 +244955,7 @@ ${indentation}`;
|
|
|
244951
244955
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
244952
244956
|
if (tmp !== void 0) {
|
|
244953
244957
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
244954
|
-
separator =
|
|
244958
|
+
separator = join5;
|
|
244955
244959
|
}
|
|
244956
244960
|
}
|
|
244957
244961
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -245009,20 +245013,20 @@ ${originalIndentation}`;
|
|
|
245009
245013
|
indentation += spacer;
|
|
245010
245014
|
let res2 = `
|
|
245011
245015
|
${indentation}`;
|
|
245012
|
-
const
|
|
245016
|
+
const join6 = `,
|
|
245013
245017
|
${indentation}`;
|
|
245014
245018
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
245015
245019
|
let i = 0;
|
|
245016
245020
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
245017
245021
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
245018
245022
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
245019
|
-
res2 +=
|
|
245023
|
+
res2 += join6;
|
|
245020
245024
|
}
|
|
245021
245025
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
245022
245026
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
245023
245027
|
if (value.length - 1 > maximumBreadth) {
|
|
245024
245028
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
245025
|
-
res2 += `${
|
|
245029
|
+
res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
245026
245030
|
}
|
|
245027
245031
|
res2 += `
|
|
245028
245032
|
${originalIndentation}`;
|
|
@@ -245038,16 +245042,16 @@ ${originalIndentation}`;
|
|
|
245038
245042
|
return '"[Object]"';
|
|
245039
245043
|
}
|
|
245040
245044
|
indentation += spacer;
|
|
245041
|
-
const
|
|
245045
|
+
const join5 = `,
|
|
245042
245046
|
${indentation}`;
|
|
245043
245047
|
let res = "";
|
|
245044
245048
|
let separator = "";
|
|
245045
245049
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
245046
245050
|
if (isTypedArrayWithEntries(value)) {
|
|
245047
|
-
res += stringifyTypedArray(value,
|
|
245051
|
+
res += stringifyTypedArray(value, join5, maximumBreadth);
|
|
245048
245052
|
keys = keys.slice(value.length);
|
|
245049
245053
|
maximumPropertiesToStringify -= value.length;
|
|
245050
|
-
separator =
|
|
245054
|
+
separator = join5;
|
|
245051
245055
|
}
|
|
245052
245056
|
if (deterministic) {
|
|
245053
245057
|
keys = sort(keys, comparator);
|
|
@@ -245058,13 +245062,13 @@ ${indentation}`;
|
|
|
245058
245062
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
245059
245063
|
if (tmp !== void 0) {
|
|
245060
245064
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
245061
|
-
separator =
|
|
245065
|
+
separator = join5;
|
|
245062
245066
|
}
|
|
245063
245067
|
}
|
|
245064
245068
|
if (keyLength > maximumBreadth) {
|
|
245065
245069
|
const removedKeys = keyLength - maximumBreadth;
|
|
245066
245070
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
245067
|
-
separator =
|
|
245071
|
+
separator = join5;
|
|
245068
245072
|
}
|
|
245069
245073
|
if (separator !== "") {
|
|
245070
245074
|
res = `
|
|
@@ -285421,11 +285425,11 @@ var require_core = __commonJS({
|
|
|
285421
285425
|
function mapOuter(r, f) {
|
|
285422
285426
|
return r.matched ? f(r) : r;
|
|
285423
285427
|
}
|
|
285424
|
-
function ab(pa, pb,
|
|
285425
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
285428
|
+
function ab(pa, pb, join5) {
|
|
285429
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
|
|
285426
285430
|
}
|
|
285427
|
-
function abc(pa, pb, pc,
|
|
285428
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
285431
|
+
function abc(pa, pb, pc, join5) {
|
|
285432
|
+
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
285433
|
}
|
|
285430
285434
|
function action(f) {
|
|
285431
285435
|
return (data, i) => {
|
|
@@ -312619,7 +312623,7 @@ var require_utils2 = __commonJS({
|
|
|
312619
312623
|
module2.exports = Utils;
|
|
312620
312624
|
Utils.prototype.makeDir = function(folder) {
|
|
312621
312625
|
const self = this;
|
|
312622
|
-
function
|
|
312626
|
+
function mkdirSync4(fpath) {
|
|
312623
312627
|
let resolvedPath = fpath.split(self.sep)[0];
|
|
312624
312628
|
fpath.split(self.sep).forEach(function(name) {
|
|
312625
312629
|
if (!name || name.substr(-1, 1) === ":") return;
|
|
@@ -312637,7 +312641,7 @@ var require_utils2 = __commonJS({
|
|
|
312637
312641
|
if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
|
|
312638
312642
|
});
|
|
312639
312643
|
}
|
|
312640
|
-
|
|
312644
|
+
mkdirSync4(folder);
|
|
312641
312645
|
};
|
|
312642
312646
|
Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
|
|
312643
312647
|
const self = this;
|
|
@@ -314256,8 +314260,8 @@ var require_adm_zip = __commonJS({
|
|
|
314256
314260
|
return null;
|
|
314257
314261
|
}
|
|
314258
314262
|
function fixPath(zipPath) {
|
|
314259
|
-
const { join:
|
|
314260
|
-
return
|
|
314263
|
+
const { join: join5, normalize, sep } = pth.posix;
|
|
314264
|
+
return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
314261
314265
|
}
|
|
314262
314266
|
function filenameFilter(filterfn) {
|
|
314263
314267
|
if (filterfn instanceof RegExp) {
|
|
@@ -319670,14 +319674,14 @@ var require_path_arg = __commonJS({
|
|
|
319670
319674
|
var require_find_made = __commonJS({
|
|
319671
319675
|
"../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports2, module2) {
|
|
319672
319676
|
"use strict";
|
|
319673
|
-
var { dirname } = require("path");
|
|
319677
|
+
var { dirname: dirname2 } = require("path");
|
|
319674
319678
|
var findMade = (opts, parent, path = void 0) => {
|
|
319675
319679
|
if (path === parent)
|
|
319676
319680
|
return Promise.resolve();
|
|
319677
319681
|
return opts.statAsync(parent).then(
|
|
319678
319682
|
(st) => st.isDirectory() ? path : void 0,
|
|
319679
319683
|
// will fail later
|
|
319680
|
-
(er) => er.code === "ENOENT" ? findMade(opts,
|
|
319684
|
+
(er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
|
|
319681
319685
|
);
|
|
319682
319686
|
};
|
|
319683
319687
|
var findMadeSync = (opts, parent, path = void 0) => {
|
|
@@ -319686,7 +319690,7 @@ var require_find_made = __commonJS({
|
|
|
319686
319690
|
try {
|
|
319687
319691
|
return opts.statSync(parent).isDirectory() ? path : void 0;
|
|
319688
319692
|
} catch (er) {
|
|
319689
|
-
return er.code === "ENOENT" ? findMadeSync(opts,
|
|
319693
|
+
return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
|
|
319690
319694
|
}
|
|
319691
319695
|
};
|
|
319692
319696
|
module2.exports = { findMade, findMadeSync };
|
|
@@ -319697,10 +319701,10 @@ var require_find_made = __commonJS({
|
|
|
319697
319701
|
var require_mkdirp_manual = __commonJS({
|
|
319698
319702
|
"../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports2, module2) {
|
|
319699
319703
|
"use strict";
|
|
319700
|
-
var { dirname } = require("path");
|
|
319704
|
+
var { dirname: dirname2 } = require("path");
|
|
319701
319705
|
var mkdirpManual = (path, opts, made) => {
|
|
319702
319706
|
opts.recursive = false;
|
|
319703
|
-
const parent =
|
|
319707
|
+
const parent = dirname2(path);
|
|
319704
319708
|
if (parent === path) {
|
|
319705
319709
|
return opts.mkdirAsync(path, opts).catch((er) => {
|
|
319706
319710
|
if (er.code !== "EISDIR")
|
|
@@ -319723,7 +319727,7 @@ var require_mkdirp_manual = __commonJS({
|
|
|
319723
319727
|
});
|
|
319724
319728
|
};
|
|
319725
319729
|
var mkdirpManualSync = (path, opts, made) => {
|
|
319726
|
-
const parent =
|
|
319730
|
+
const parent = dirname2(path);
|
|
319727
319731
|
opts.recursive = false;
|
|
319728
319732
|
if (parent === path) {
|
|
319729
319733
|
try {
|
|
@@ -319759,12 +319763,12 @@ var require_mkdirp_manual = __commonJS({
|
|
|
319759
319763
|
var require_mkdirp_native = __commonJS({
|
|
319760
319764
|
"../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports2, module2) {
|
|
319761
319765
|
"use strict";
|
|
319762
|
-
var { dirname } = require("path");
|
|
319766
|
+
var { dirname: dirname2 } = require("path");
|
|
319763
319767
|
var { findMade, findMadeSync } = require_find_made();
|
|
319764
319768
|
var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
|
|
319765
319769
|
var mkdirpNative = (path, opts) => {
|
|
319766
319770
|
opts.recursive = true;
|
|
319767
|
-
const parent =
|
|
319771
|
+
const parent = dirname2(path);
|
|
319768
319772
|
if (parent === path)
|
|
319769
319773
|
return opts.mkdirAsync(path, opts);
|
|
319770
319774
|
return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
|
|
@@ -319776,7 +319780,7 @@ var require_mkdirp_native = __commonJS({
|
|
|
319776
319780
|
};
|
|
319777
319781
|
var mkdirpNativeSync = (path, opts) => {
|
|
319778
319782
|
opts.recursive = true;
|
|
319779
|
-
const parent =
|
|
319783
|
+
const parent = dirname2(path);
|
|
319780
319784
|
if (parent === path)
|
|
319781
319785
|
return opts.mkdirSync(path, opts);
|
|
319782
319786
|
const made = findMadeSync(opts, path);
|
|
@@ -320181,7 +320185,7 @@ var require_path_reservations = __commonJS({
|
|
|
320181
320185
|
var assert = require("assert");
|
|
320182
320186
|
var normalize = require_normalize_unicode();
|
|
320183
320187
|
var stripSlashes = require_strip_trailing_slashes();
|
|
320184
|
-
var { join:
|
|
320188
|
+
var { join: join5 } = require("path");
|
|
320185
320189
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
320186
320190
|
var isWindows = platform3 === "win32";
|
|
320187
320191
|
module2.exports = () => {
|
|
@@ -320190,7 +320194,7 @@ var require_path_reservations = __commonJS({
|
|
|
320190
320194
|
const getDirs = (path) => {
|
|
320191
320195
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
320192
320196
|
if (set.length) {
|
|
320193
|
-
path2 =
|
|
320197
|
+
path2 = join5(set[set.length - 1], path2);
|
|
320194
320198
|
}
|
|
320195
320199
|
set.push(path2 || "/");
|
|
320196
320200
|
return set;
|
|
@@ -320258,7 +320262,7 @@ var require_path_reservations = __commonJS({
|
|
|
320258
320262
|
};
|
|
320259
320263
|
const reserve = (paths, fn) => {
|
|
320260
320264
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
320261
|
-
return stripSlashes(
|
|
320265
|
+
return stripSlashes(join5(normalize(p))).toLowerCase();
|
|
320262
320266
|
});
|
|
320263
320267
|
const dirs = new Set(
|
|
320264
320268
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -324796,7 +324800,7 @@ var require_package4 = __commonJS({
|
|
|
324796
324800
|
"package.json"(exports2, module2) {
|
|
324797
324801
|
module2.exports = {
|
|
324798
324802
|
name: "@validate.qa/runner",
|
|
324799
|
-
version: "1.0.
|
|
324803
|
+
version: "1.0.4",
|
|
324800
324804
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
324801
324805
|
bin: {
|
|
324802
324806
|
"validate-runner": "dist/cli.js"
|
|
@@ -324804,7 +324808,7 @@ var require_package4 = __commonJS({
|
|
|
324804
324808
|
scripts: {
|
|
324805
324809
|
build: "tsup",
|
|
324806
324810
|
dev: "tsx src/cli.ts",
|
|
324807
|
-
"test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
324811
|
+
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
324808
324812
|
prepublishOnly: "npm run build"
|
|
324809
324813
|
},
|
|
324810
324814
|
files: [
|
|
@@ -324882,9 +324886,9 @@ function getConfigDir() {
|
|
|
324882
324886
|
}
|
|
324883
324887
|
|
|
324884
324888
|
// src/runner.ts
|
|
324885
|
-
var
|
|
324886
|
-
var
|
|
324887
|
-
var
|
|
324889
|
+
var import_fs4 = require("fs");
|
|
324890
|
+
var import_path3 = require("path");
|
|
324891
|
+
var import_os5 = require("os");
|
|
324888
324892
|
var import_node_crypto2 = require("crypto");
|
|
324889
324893
|
|
|
324890
324894
|
// src/services/playwright-native.ts
|
|
@@ -325297,11 +325301,12 @@ function discoverRealIOSDevices() {
|
|
|
325297
325301
|
for (const [index, node] of iosNodes.entries()) {
|
|
325298
325302
|
const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
|
|
325299
325303
|
const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
|
|
325304
|
+
const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
|
|
325300
325305
|
devices.push({
|
|
325301
325306
|
id: serial,
|
|
325302
325307
|
name,
|
|
325303
325308
|
platform: "IOS",
|
|
325304
|
-
platformVersion:
|
|
325309
|
+
platformVersion: version,
|
|
325305
325310
|
state: "BOOTED",
|
|
325306
325311
|
isAvailable: true,
|
|
325307
325312
|
isPhysical: true
|
|
@@ -325376,7 +325381,7 @@ function parseAdbDevices() {
|
|
|
325376
325381
|
const output = runCommand("adb", ["devices", "-l"]);
|
|
325377
325382
|
if (!output) return [];
|
|
325378
325383
|
const lines = output.split("\n").slice(1);
|
|
325379
|
-
return lines.filter((line) => line.
|
|
325384
|
+
return lines.filter((line) => line.trim().length > 0).map((line) => {
|
|
325380
325385
|
const parts = line.trim().split(/\s+/);
|
|
325381
325386
|
const serial = parts[0];
|
|
325382
325387
|
const type = parts[1] ?? "";
|
|
@@ -325392,26 +325397,25 @@ function parseAdbDevices() {
|
|
|
325392
325397
|
device: props["device"],
|
|
325393
325398
|
transportId: props["transport_id"]
|
|
325394
325399
|
};
|
|
325395
|
-
}).filter((d) => d.type === "device");
|
|
325400
|
+
}).filter((d) => d.type === "device" || d.type === "unauthorized");
|
|
325396
325401
|
}
|
|
325397
325402
|
function getAndroidProp(serial, prop) {
|
|
325398
325403
|
return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
|
|
325399
325404
|
}
|
|
325400
325405
|
function discoverAndroidDevices() {
|
|
325401
325406
|
const adbDevices = parseAdbDevices();
|
|
325402
|
-
return adbDevices.filter((d) => {
|
|
325403
|
-
return d.type === "device";
|
|
325404
|
-
}).map((d) => {
|
|
325407
|
+
return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
|
|
325405
325408
|
const isEmulator = d.serial.startsWith("emulator-");
|
|
325409
|
+
const isAvailable = d.type === "device";
|
|
325406
325410
|
const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
|
|
325407
|
-
const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
|
|
325411
|
+
const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
|
|
325408
325412
|
return {
|
|
325409
325413
|
id: d.serial,
|
|
325410
325414
|
name,
|
|
325411
325415
|
platform: "ANDROID",
|
|
325412
325416
|
platformVersion,
|
|
325413
325417
|
state: "BOOTED",
|
|
325414
|
-
isAvailable
|
|
325418
|
+
isAvailable,
|
|
325415
325419
|
// Mark if it's a real device vs emulator
|
|
325416
325420
|
...isEmulator ? {} : { isPhysical: true }
|
|
325417
325421
|
};
|
|
@@ -325444,10 +325448,14 @@ function discoverAllDevices(options) {
|
|
|
325444
325448
|
const androidDevices = discoverAndroidDevices();
|
|
325445
325449
|
if (options?.includeApps) {
|
|
325446
325450
|
for (const device of iosDevices) {
|
|
325447
|
-
|
|
325451
|
+
if (device.isAvailable) {
|
|
325452
|
+
device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
|
|
325453
|
+
}
|
|
325448
325454
|
}
|
|
325449
325455
|
for (const device of androidDevices) {
|
|
325450
|
-
|
|
325456
|
+
if (device.isAvailable) {
|
|
325457
|
+
device.installedApps = listAndroidInstalledApps(device.id);
|
|
325458
|
+
}
|
|
325451
325459
|
}
|
|
325452
325460
|
}
|
|
325453
325461
|
return [...iosDevices, ...androidDevices];
|
|
@@ -325455,11 +325463,12 @@ function discoverAllDevices(options) {
|
|
|
325455
325463
|
function getRunnerCapabilities() {
|
|
325456
325464
|
const iosDevices = discoverIOSDevices();
|
|
325457
325465
|
const androidDevices = discoverAndroidDevices();
|
|
325466
|
+
const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
|
|
325458
325467
|
return {
|
|
325459
325468
|
web: true,
|
|
325460
325469
|
// Always capable of web testing via Playwright
|
|
325461
|
-
ios: iosDevices.
|
|
325462
|
-
android: androidDevices.
|
|
325470
|
+
ios: iosDevices.some(isReadyDevice),
|
|
325471
|
+
android: androidDevices.some(isReadyDevice)
|
|
325463
325472
|
};
|
|
325464
325473
|
}
|
|
325465
325474
|
|
|
@@ -325861,7 +325870,7 @@ async function executeMobileTest(options) {
|
|
|
325861
325870
|
const devices = discoverAllDevices({ includeApps: true });
|
|
325862
325871
|
const platformFilter = options.target.platform;
|
|
325863
325872
|
const matchingDevices = devices.filter(
|
|
325864
|
-
(d) => d.platform === platformFilter && d.state === "BOOTED"
|
|
325873
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
325865
325874
|
);
|
|
325866
325875
|
if (matchingDevices.length === 0) {
|
|
325867
325876
|
throw new Error(
|
|
@@ -326025,7 +326034,7 @@ async function createMobileDiscoverySession(target) {
|
|
|
326025
326034
|
const devices = discoverAllDevices({ includeApps: true });
|
|
326026
326035
|
const platformFilter = target.platform;
|
|
326027
326036
|
const matchingDevices = devices.filter(
|
|
326028
|
-
(d) => d.platform === platformFilter && d.state === "BOOTED"
|
|
326037
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
326029
326038
|
);
|
|
326030
326039
|
if (matchingDevices.length === 0) {
|
|
326031
326040
|
throw new Error(
|
|
@@ -326076,6 +326085,9 @@ async function createMobileDiscoverySession(target) {
|
|
|
326076
326085
|
var import_http = require("http");
|
|
326077
326086
|
var import_child_process3 = require("child_process");
|
|
326078
326087
|
var import_crypto = require("crypto");
|
|
326088
|
+
var import_os3 = require("os");
|
|
326089
|
+
var import_path2 = require("path");
|
|
326090
|
+
var import_fs2 = require("fs");
|
|
326079
326091
|
init_dist();
|
|
326080
326092
|
|
|
326081
326093
|
// src/services/mobile/appium-actions.ts
|
|
@@ -326306,6 +326318,7 @@ var polling = false;
|
|
|
326306
326318
|
var sessionStartInFlight = null;
|
|
326307
326319
|
var sessionReaperTimer = null;
|
|
326308
326320
|
var mirrorClientsEmptyAt = null;
|
|
326321
|
+
var mirrorSessionOwner = null;
|
|
326309
326322
|
var executorBusy = false;
|
|
326310
326323
|
var rateLimitBuckets = /* @__PURE__ */ new Map();
|
|
326311
326324
|
function isRateLimited(clientIp) {
|
|
@@ -326361,30 +326374,50 @@ async function getWindowSize3() {
|
|
|
326361
326374
|
return getWindowSize2(`/session/${state.appiumSessionId}`);
|
|
326362
326375
|
}
|
|
326363
326376
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
326364
|
-
|
|
326365
|
-
|
|
326377
|
+
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
326378
|
+
var BRIDGE_PID_FILE = (0, import_path2.join)((0, import_os3.homedir)(), ".validate.qa", "mobile-bridge.pid");
|
|
326379
|
+
function readBridgePidFile() {
|
|
326366
326380
|
try {
|
|
326367
|
-
const
|
|
326368
|
-
|
|
326369
|
-
|
|
326370
|
-
|
|
326371
|
-
|
|
326372
|
-
|
|
326373
|
-
|
|
326374
|
-
|
|
326375
|
-
|
|
326376
|
-
|
|
326377
|
-
|
|
326378
|
-
|
|
326379
|
-
|
|
326380
|
-
|
|
326381
|
-
|
|
326382
|
-
|
|
326383
|
-
|
|
326384
|
-
|
|
326385
|
-
|
|
326381
|
+
const raw = (0, import_fs2.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
|
|
326382
|
+
const pid = parseInt(raw, 10);
|
|
326383
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
326384
|
+
} catch {
|
|
326385
|
+
return null;
|
|
326386
|
+
}
|
|
326387
|
+
}
|
|
326388
|
+
function writeBridgePidFile(pid) {
|
|
326389
|
+
try {
|
|
326390
|
+
(0, import_fs2.mkdirSync)((0, import_path2.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
|
|
326391
|
+
(0, import_fs2.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
|
|
326392
|
+
} catch {
|
|
326393
|
+
}
|
|
326394
|
+
}
|
|
326395
|
+
function clearBridgePidFile() {
|
|
326396
|
+
try {
|
|
326397
|
+
(0, import_fs2.unlinkSync)(BRIDGE_PID_FILE);
|
|
326398
|
+
} catch {
|
|
326399
|
+
}
|
|
326400
|
+
}
|
|
326401
|
+
function killStaleBridge() {
|
|
326402
|
+
const pid = readBridgePidFile();
|
|
326403
|
+
if (!pid) return;
|
|
326404
|
+
try {
|
|
326405
|
+
(0, import_child_process3.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
|
|
326406
|
+
} catch {
|
|
326407
|
+
clearBridgePidFile();
|
|
326408
|
+
return;
|
|
326409
|
+
}
|
|
326410
|
+
try {
|
|
326411
|
+
const pidsOnPort = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
|
|
326412
|
+
const pidList = pidsOnPort.split("\n").filter(Boolean);
|
|
326413
|
+
if (!pidList.includes(String(pid))) {
|
|
326414
|
+
return;
|
|
326386
326415
|
}
|
|
326416
|
+
(0, import_child_process3.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
|
|
326417
|
+
(0, import_child_process3.execFileSync)("sleep", ["1"], { stdio: "ignore" });
|
|
326387
326418
|
} catch {
|
|
326419
|
+
} finally {
|
|
326420
|
+
clearBridgePidFile();
|
|
326388
326421
|
}
|
|
326389
326422
|
}
|
|
326390
326423
|
function parseBody(req) {
|
|
@@ -326622,6 +326655,7 @@ async function startSession(validated, res) {
|
|
|
326622
326655
|
}
|
|
326623
326656
|
state.appiumSessionId = null;
|
|
326624
326657
|
state.platform = null;
|
|
326658
|
+
mirrorSessionOwner = null;
|
|
326625
326659
|
}
|
|
326626
326660
|
stopAllMirrorClients();
|
|
326627
326661
|
const realCaps = isPhysicalDevice(deviceId, targetPlatform) ? realDeviceCapabilities2(targetPlatform) : {};
|
|
@@ -326652,11 +326686,14 @@ async function startSession(validated, res) {
|
|
|
326652
326686
|
})
|
|
326653
326687
|
});
|
|
326654
326688
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
326655
|
-
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
326656
326689
|
if (!state.appiumSessionId) {
|
|
326690
|
+
state.platform = null;
|
|
326691
|
+
mirrorSessionOwner = null;
|
|
326657
326692
|
sendJSON(res, 500, { error: "Appium session creation returned no session ID" });
|
|
326658
326693
|
return;
|
|
326659
326694
|
}
|
|
326695
|
+
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
326696
|
+
mirrorSessionOwner = "recording";
|
|
326660
326697
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
326661
326698
|
try {
|
|
326662
326699
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -326681,6 +326718,7 @@ async function handleSessionStop(_req, res) {
|
|
|
326681
326718
|
}
|
|
326682
326719
|
state.appiumSessionId = null;
|
|
326683
326720
|
state.platform = null;
|
|
326721
|
+
mirrorSessionOwner = null;
|
|
326684
326722
|
}
|
|
326685
326723
|
stopAllMirrorClients();
|
|
326686
326724
|
stopSessionReaper();
|
|
@@ -326761,6 +326799,7 @@ async function pollAndBroadcastFrame() {
|
|
|
326761
326799
|
if (!alive) {
|
|
326762
326800
|
state.appiumSessionId = null;
|
|
326763
326801
|
state.platform = null;
|
|
326802
|
+
mirrorSessionOwner = null;
|
|
326764
326803
|
stopAllMirrorClients();
|
|
326765
326804
|
return;
|
|
326766
326805
|
}
|
|
@@ -326831,6 +326870,7 @@ async function reapIdleSession() {
|
|
|
326831
326870
|
const sessionId = state.appiumSessionId;
|
|
326832
326871
|
state.appiumSessionId = null;
|
|
326833
326872
|
state.platform = null;
|
|
326873
|
+
mirrorSessionOwner = null;
|
|
326834
326874
|
mirrorClientsEmptyAt = null;
|
|
326835
326875
|
stopSessionReaper();
|
|
326836
326876
|
try {
|
|
@@ -327009,7 +327049,7 @@ async function startMobileBridge(options) {
|
|
|
327009
327049
|
if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
|
|
327010
327050
|
process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
|
|
327011
327051
|
}
|
|
327012
|
-
|
|
327052
|
+
killStaleBridge();
|
|
327013
327053
|
return new Promise((resolve, reject) => {
|
|
327014
327054
|
server2 = (0, import_http.createServer)(route);
|
|
327015
327055
|
server2.on("error", (err) => {
|
|
@@ -327025,6 +327065,7 @@ async function startMobileBridge(options) {
|
|
|
327025
327065
|
state.port = port;
|
|
327026
327066
|
state.tunnelToken = tunnelToken;
|
|
327027
327067
|
state.tunnelUrl = void 0;
|
|
327068
|
+
writeBridgePidFile(process.pid);
|
|
327028
327069
|
let tunnelUrl;
|
|
327029
327070
|
if (enableTunnel) {
|
|
327030
327071
|
try {
|
|
@@ -327092,6 +327133,7 @@ async function startMobileBridge(options) {
|
|
|
327092
327133
|
let checkInterval = null;
|
|
327093
327134
|
const timeout = setTimeout(() => {
|
|
327094
327135
|
if (checkInterval) clearInterval(checkInterval);
|
|
327136
|
+
checkInterval = null;
|
|
327095
327137
|
if (!tunnelUrl) {
|
|
327096
327138
|
reject2(new Error("ngrok tunnel failed to start"));
|
|
327097
327139
|
}
|
|
@@ -327100,6 +327142,7 @@ async function startMobileBridge(options) {
|
|
|
327100
327142
|
if (tunnelUrl) {
|
|
327101
327143
|
clearTimeout(timeout);
|
|
327102
327144
|
if (checkInterval) clearInterval(checkInterval);
|
|
327145
|
+
checkInterval = null;
|
|
327103
327146
|
resolve2();
|
|
327104
327147
|
}
|
|
327105
327148
|
}, 500);
|
|
@@ -327129,6 +327172,7 @@ async function startMobileBridge(options) {
|
|
|
327129
327172
|
async function stopMobileBridge() {
|
|
327130
327173
|
stopAllMirrorClients();
|
|
327131
327174
|
stopSessionReaper();
|
|
327175
|
+
clearBridgePidFile();
|
|
327132
327176
|
if (state.appiumSessionId) {
|
|
327133
327177
|
try {
|
|
327134
327178
|
validateSessionId(state.appiumSessionId);
|
|
@@ -327137,6 +327181,7 @@ async function stopMobileBridge() {
|
|
|
327137
327181
|
}
|
|
327138
327182
|
state.appiumSessionId = null;
|
|
327139
327183
|
state.platform = null;
|
|
327184
|
+
mirrorSessionOwner = null;
|
|
327140
327185
|
}
|
|
327141
327186
|
if (ngrokProcess) {
|
|
327142
327187
|
try {
|
|
@@ -327167,13 +327212,37 @@ function getBridgeState() {
|
|
|
327167
327212
|
tunnelUrl: state.tunnelUrl,
|
|
327168
327213
|
tunnelToken: state.tunnelToken,
|
|
327169
327214
|
hasActiveSession: !!state.appiumSessionId,
|
|
327170
|
-
recordingActive: !!state.appiumSessionId,
|
|
327215
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording",
|
|
327171
327216
|
platform: state.platform
|
|
327172
327217
|
};
|
|
327173
327218
|
}
|
|
327174
327219
|
function setExecutorBusy(busy) {
|
|
327175
327220
|
executorBusy = busy;
|
|
327176
327221
|
}
|
|
327222
|
+
function attachMirrorSession(sessionId, platform3) {
|
|
327223
|
+
validateSessionId(sessionId);
|
|
327224
|
+
if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
|
|
327225
|
+
stopAllMirrorClients();
|
|
327226
|
+
}
|
|
327227
|
+
state.appiumSessionId = sessionId;
|
|
327228
|
+
state.platform = platform3;
|
|
327229
|
+
mirrorSessionOwner = "runner";
|
|
327230
|
+
mirrorNullTicks = 0;
|
|
327231
|
+
mirrorClientsEmptyAt = null;
|
|
327232
|
+
ensureSessionReaper();
|
|
327233
|
+
if (state.mirrorClients.size > 0) {
|
|
327234
|
+
ensureMirrorPolling();
|
|
327235
|
+
void pollAndBroadcastFrame();
|
|
327236
|
+
}
|
|
327237
|
+
}
|
|
327238
|
+
function detachMirrorSession(sessionId) {
|
|
327239
|
+
if (sessionId && state.appiumSessionId && state.appiumSessionId !== sessionId) return;
|
|
327240
|
+
state.appiumSessionId = null;
|
|
327241
|
+
state.platform = null;
|
|
327242
|
+
mirrorSessionOwner = null;
|
|
327243
|
+
stopAllMirrorClients();
|
|
327244
|
+
stopSessionReaper();
|
|
327245
|
+
}
|
|
327177
327246
|
|
|
327178
327247
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
327179
327248
|
var import_runner_core7 = __toESM(require_dist2());
|
|
@@ -327989,6 +328058,219 @@ async function startMobileNetworkCapture(options) {
|
|
|
327989
328058
|
};
|
|
327990
328059
|
}
|
|
327991
328060
|
|
|
328061
|
+
// src/services/doctor.ts
|
|
328062
|
+
var import_child_process4 = require("child_process");
|
|
328063
|
+
var import_fs3 = require("fs");
|
|
328064
|
+
var import_os4 = require("os");
|
|
328065
|
+
var C = {
|
|
328066
|
+
reset: "\x1B[0m",
|
|
328067
|
+
bold: "\x1B[1m",
|
|
328068
|
+
green: "\x1B[32m",
|
|
328069
|
+
red: "\x1B[31m",
|
|
328070
|
+
yellow: "\x1B[33m",
|
|
328071
|
+
cyan: "\x1B[36m",
|
|
328072
|
+
gray: "\x1B[90m"
|
|
328073
|
+
};
|
|
328074
|
+
function run(cmd) {
|
|
328075
|
+
try {
|
|
328076
|
+
return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
328077
|
+
} catch {
|
|
328078
|
+
return null;
|
|
328079
|
+
}
|
|
328080
|
+
}
|
|
328081
|
+
function check(name, fn) {
|
|
328082
|
+
try {
|
|
328083
|
+
const result = fn();
|
|
328084
|
+
return { name, ...result };
|
|
328085
|
+
} catch (err) {
|
|
328086
|
+
return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
|
|
328087
|
+
}
|
|
328088
|
+
}
|
|
328089
|
+
function checkWeb() {
|
|
328090
|
+
const results = [];
|
|
328091
|
+
results.push(check("Node.js", () => {
|
|
328092
|
+
const version = process.version;
|
|
328093
|
+
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
328094
|
+
return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
|
|
328095
|
+
}));
|
|
328096
|
+
results.push(check("Playwright", () => {
|
|
328097
|
+
const version = run("npx playwright --version");
|
|
328098
|
+
if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
|
|
328099
|
+
return { ok: true, detail: version };
|
|
328100
|
+
}));
|
|
328101
|
+
return results;
|
|
328102
|
+
}
|
|
328103
|
+
function checkIOS() {
|
|
328104
|
+
const results = [];
|
|
328105
|
+
const isMac = (0, import_os4.platform)() === "darwin";
|
|
328106
|
+
results.push(check("macOS", () => ({
|
|
328107
|
+
ok: isMac,
|
|
328108
|
+
detail: isMac ? `${(0, import_os4.platform)()} detected` : "iOS testing requires macOS"
|
|
328109
|
+
})));
|
|
328110
|
+
if (!isMac) return results;
|
|
328111
|
+
results.push(check("Xcode", () => {
|
|
328112
|
+
const version = run("xcodebuild -version");
|
|
328113
|
+
if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
|
|
328114
|
+
const firstLine = version.split("\n")[0];
|
|
328115
|
+
return { ok: true, detail: firstLine };
|
|
328116
|
+
}));
|
|
328117
|
+
results.push(check("Xcode Command Line Tools", () => {
|
|
328118
|
+
const path = run("xcode-select -p");
|
|
328119
|
+
return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
|
|
328120
|
+
}));
|
|
328121
|
+
results.push(check("Appium xcuitest driver", () => {
|
|
328122
|
+
const drivers = run("appium driver list --installed --json");
|
|
328123
|
+
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
328124
|
+
try {
|
|
328125
|
+
const parsed = JSON.parse(drivers);
|
|
328126
|
+
const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
|
|
328127
|
+
return {
|
|
328128
|
+
ok: !!hasXcuitest,
|
|
328129
|
+
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
328130
|
+
};
|
|
328131
|
+
} catch {
|
|
328132
|
+
const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
|
|
328133
|
+
return {
|
|
328134
|
+
ok: hasXcuitest,
|
|
328135
|
+
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
328136
|
+
};
|
|
328137
|
+
}
|
|
328138
|
+
}));
|
|
328139
|
+
results.push(check("iOS Devices", () => {
|
|
328140
|
+
const json = run("xcrun simctl list devices booted --json");
|
|
328141
|
+
let bootedSimulators = 0;
|
|
328142
|
+
if (json) {
|
|
328143
|
+
try {
|
|
328144
|
+
const parsed = JSON.parse(json);
|
|
328145
|
+
const devices = Object.values(parsed.devices ?? {}).flat();
|
|
328146
|
+
bootedSimulators = devices.filter((d) => d.state === "Booted").length;
|
|
328147
|
+
} catch {
|
|
328148
|
+
}
|
|
328149
|
+
}
|
|
328150
|
+
const realDevices = discoverRealIOSDevices().length;
|
|
328151
|
+
const total = bootedSimulators + realDevices;
|
|
328152
|
+
if (total === 0) {
|
|
328153
|
+
return {
|
|
328154
|
+
ok: false,
|
|
328155
|
+
detail: "No devices found. Boot a simulator or connect a real device via USB."
|
|
328156
|
+
};
|
|
328157
|
+
}
|
|
328158
|
+
const details = [];
|
|
328159
|
+
if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
|
|
328160
|
+
if (realDevices > 0) details.push(`${realDevices} real device(s)`);
|
|
328161
|
+
return {
|
|
328162
|
+
ok: true,
|
|
328163
|
+
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
328164
|
+
};
|
|
328165
|
+
}));
|
|
328166
|
+
return results;
|
|
328167
|
+
}
|
|
328168
|
+
function checkAndroid() {
|
|
328169
|
+
const results = [];
|
|
328170
|
+
const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
328171
|
+
results.push(check("ANDROID_HOME", () => ({
|
|
328172
|
+
ok: !!androidHome && (0, import_fs3.existsSync)(androidHome),
|
|
328173
|
+
detail: androidHome ? (0, import_fs3.existsSync)(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
|
|
328174
|
+
})));
|
|
328175
|
+
if (!androidHome || !(0, import_fs3.existsSync)(androidHome)) return results;
|
|
328176
|
+
results.push(check("adb", () => {
|
|
328177
|
+
const version = run("adb version");
|
|
328178
|
+
if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
|
|
328179
|
+
const firstLine = version.split("\n")[0];
|
|
328180
|
+
return { ok: true, detail: firstLine };
|
|
328181
|
+
}));
|
|
328182
|
+
results.push(check("Appium uiautomator2 driver", () => {
|
|
328183
|
+
const drivers = run("appium driver list --installed --json");
|
|
328184
|
+
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
328185
|
+
try {
|
|
328186
|
+
const parsed = JSON.parse(drivers);
|
|
328187
|
+
const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
|
|
328188
|
+
return {
|
|
328189
|
+
ok: !!hasUia2,
|
|
328190
|
+
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
328191
|
+
};
|
|
328192
|
+
} catch {
|
|
328193
|
+
const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
|
|
328194
|
+
return {
|
|
328195
|
+
ok: hasUia2,
|
|
328196
|
+
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
328197
|
+
};
|
|
328198
|
+
}
|
|
328199
|
+
}));
|
|
328200
|
+
results.push(check("Android Devices", () => {
|
|
328201
|
+
const output = run("adb devices");
|
|
328202
|
+
if (!output) return { ok: false, detail: "adb devices failed" };
|
|
328203
|
+
const lines = output.split("\n").filter((l) => l.includes(" device"));
|
|
328204
|
+
const emulators = lines.filter((l) => l.startsWith("emulator-"));
|
|
328205
|
+
const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
|
|
328206
|
+
const total = emulators.length + realDevices.length;
|
|
328207
|
+
if (total === 0) {
|
|
328208
|
+
return {
|
|
328209
|
+
ok: false,
|
|
328210
|
+
detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
|
|
328211
|
+
};
|
|
328212
|
+
}
|
|
328213
|
+
const details = [];
|
|
328214
|
+
if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
|
|
328215
|
+
if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
|
|
328216
|
+
return {
|
|
328217
|
+
ok: true,
|
|
328218
|
+
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
328219
|
+
};
|
|
328220
|
+
}));
|
|
328221
|
+
return results;
|
|
328222
|
+
}
|
|
328223
|
+
function checkTunnel() {
|
|
328224
|
+
const version = run("ngrok version") || run("ngrok --version");
|
|
328225
|
+
if (!version) {
|
|
328226
|
+
return [{
|
|
328227
|
+
name: "ngrok",
|
|
328228
|
+
ok: false,
|
|
328229
|
+
detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
|
|
328230
|
+
}];
|
|
328231
|
+
}
|
|
328232
|
+
const match = version.match(/(\d+)\.\d+\.\d+/);
|
|
328233
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
328234
|
+
return [{
|
|
328235
|
+
name: "ngrok",
|
|
328236
|
+
ok: major >= 3,
|
|
328237
|
+
detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
|
|
328238
|
+
}];
|
|
328239
|
+
}
|
|
328240
|
+
function runDoctor() {
|
|
328241
|
+
const web = checkWeb();
|
|
328242
|
+
const ios = checkIOS();
|
|
328243
|
+
const android = checkAndroid();
|
|
328244
|
+
const tunnel = checkTunnel();
|
|
328245
|
+
const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
|
|
328246
|
+
return { web, ios, android, tunnel, allPassed };
|
|
328247
|
+
}
|
|
328248
|
+
function printDoctorReport(report) {
|
|
328249
|
+
console.log("");
|
|
328250
|
+
console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
|
|
328251
|
+
console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
|
|
328252
|
+
const printSection = (title, checks) => {
|
|
328253
|
+
console.log("");
|
|
328254
|
+
console.log(` ${C.bold}${title}:${C.reset}`);
|
|
328255
|
+
for (const c of checks) {
|
|
328256
|
+
const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
|
|
328257
|
+
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
328258
|
+
}
|
|
328259
|
+
};
|
|
328260
|
+
printSection("Web Testing", report.web);
|
|
328261
|
+
printSection("iOS Testing", report.ios);
|
|
328262
|
+
printSection("Android Testing", report.android);
|
|
328263
|
+
printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
|
|
328264
|
+
console.log("");
|
|
328265
|
+
if (report.allPassed) {
|
|
328266
|
+
console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
|
|
328267
|
+
} else {
|
|
328268
|
+
const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
|
|
328269
|
+
console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
|
|
328270
|
+
}
|
|
328271
|
+
console.log("");
|
|
328272
|
+
}
|
|
328273
|
+
|
|
327992
328274
|
// src/runner.ts
|
|
327993
328275
|
init_dist();
|
|
327994
328276
|
var import_runner_core11 = __toESM(require_dist2());
|
|
@@ -328078,7 +328360,7 @@ var browserPool = new import_runner_core6.BrowserPool({
|
|
|
328078
328360
|
maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
|
|
328079
328361
|
retireAfterContexts: 30
|
|
328080
328362
|
});
|
|
328081
|
-
var
|
|
328363
|
+
var C2 = {
|
|
328082
328364
|
reset: "\x1B[0m",
|
|
328083
328365
|
bold: "\x1B[1m",
|
|
328084
328366
|
green: "\x1B[32m",
|
|
@@ -328088,25 +328370,25 @@ var C = {
|
|
|
328088
328370
|
gray: "\x1B[90m"
|
|
328089
328371
|
};
|
|
328090
328372
|
var log = {
|
|
328091
|
-
info: (msg) => console.log(`${
|
|
328092
|
-
ok: (msg) => console.log(`${
|
|
328093
|
-
fail: (msg) => console.log(`${
|
|
328094
|
-
warn: (msg) => console.log(`${
|
|
328095
|
-
dim: (msg) => console.log(`${
|
|
328373
|
+
info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
|
|
328374
|
+
ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
|
|
328375
|
+
fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
|
|
328376
|
+
warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
|
|
328377
|
+
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
328096
328378
|
};
|
|
328097
328379
|
var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
328098
328380
|
function getRunsDir() {
|
|
328099
|
-
const dir = process.env.RUNS_DIR || (0,
|
|
328100
|
-
if (!(0,
|
|
328381
|
+
const dir = process.env.RUNS_DIR || (0, import_path3.join)((0, import_os5.homedir)(), ".validate.qa", "runs");
|
|
328382
|
+
if (!(0, import_fs4.existsSync)(dir)) (0, import_fs4.mkdirSync)(dir, { recursive: true });
|
|
328101
328383
|
return dir;
|
|
328102
328384
|
}
|
|
328103
328385
|
function authStatePath(projectId) {
|
|
328104
|
-
return (0,
|
|
328386
|
+
return (0, import_path3.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
|
|
328105
328387
|
}
|
|
328106
328388
|
function getValidAuthState(projectId) {
|
|
328107
328389
|
const path = authStatePath(projectId);
|
|
328108
|
-
if (!(0,
|
|
328109
|
-
const age = Date.now() - (0,
|
|
328390
|
+
if (!(0, import_fs4.existsSync)(path)) return void 0;
|
|
328391
|
+
const age = Date.now() - (0, import_fs4.statSync)(path).mtimeMs;
|
|
328110
328392
|
return age < AUTH_MAX_AGE_MS ? path : void 0;
|
|
328111
328393
|
}
|
|
328112
328394
|
function buildHeaders(authToken, projectId) {
|
|
@@ -329048,9 +329330,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329048
329330
|
(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
329331
|
);
|
|
329050
329332
|
if (phaseLines.length > 0) {
|
|
329051
|
-
console.log(`${
|
|
329333
|
+
console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
|
|
329052
329334
|
for (const line of phaseLines) console.log(line);
|
|
329053
|
-
console.log(`${
|
|
329335
|
+
console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
|
|
329054
329336
|
}
|
|
329055
329337
|
}
|
|
329056
329338
|
log[result2.passed ? "ok" : "fail"](
|
|
@@ -329589,11 +329871,18 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329589
329871
|
}
|
|
329590
329872
|
try {
|
|
329591
329873
|
if (isNativeMobile) {
|
|
329592
|
-
const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
|
|
329593
329874
|
const mobileTarget = ctx.target;
|
|
329594
329875
|
if (!mobileTarget || !mobileTarget.appId) {
|
|
329595
329876
|
throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
|
|
329596
329877
|
}
|
|
329878
|
+
const targetPlatform = mobileTarget.platform;
|
|
329879
|
+
const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
|
|
329880
|
+
if (!platform3) {
|
|
329881
|
+
throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
|
|
329882
|
+
}
|
|
329883
|
+
if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
|
|
329884
|
+
throw new Error("Native mobile discovery medium does not match target.platform.");
|
|
329885
|
+
}
|
|
329597
329886
|
if (getBridgeState().hasActiveSession) {
|
|
329598
329887
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
329599
329888
|
}
|
|
@@ -329612,6 +329901,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329612
329901
|
deeplink: mobileTarget.deeplink
|
|
329613
329902
|
});
|
|
329614
329903
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
329904
|
+
attachMirrorSession(session.sessionId, platform3);
|
|
329615
329905
|
const driver = new MobileBridgeDriver({
|
|
329616
329906
|
sessionId: session.sessionId,
|
|
329617
329907
|
platform: platform3,
|
|
@@ -329652,6 +329942,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329652
329942
|
}
|
|
329653
329943
|
}
|
|
329654
329944
|
if (session) {
|
|
329945
|
+
detachMirrorSession(session.sessionId);
|
|
329655
329946
|
try {
|
|
329656
329947
|
await session.stop();
|
|
329657
329948
|
} catch {
|
|
@@ -330022,9 +330313,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330022
330313
|
} else {
|
|
330023
330314
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
330024
330315
|
if (result.logs) {
|
|
330025
|
-
console.log(`${
|
|
330316
|
+
console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
|
|
330026
330317
|
console.log(result.logs);
|
|
330027
|
-
console.log(`${
|
|
330318
|
+
console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
|
|
330028
330319
|
}
|
|
330029
330320
|
}
|
|
330030
330321
|
} catch (err) {
|
|
@@ -330046,20 +330337,24 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330046
330337
|
}
|
|
330047
330338
|
async function startRunner(opts) {
|
|
330048
330339
|
const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
|
|
330340
|
+
process.env.SERVER_URL = serverUrl;
|
|
330341
|
+
process.env.AUTH_TOKEN = authToken;
|
|
330342
|
+
process.env.RUNNER_TOKEN = authToken;
|
|
330343
|
+
if (projectId) process.env.PROJECT_ID = projectId;
|
|
330049
330344
|
const headers = buildHeaders(authToken, projectId);
|
|
330050
330345
|
console.log("");
|
|
330051
|
-
console.log(`${
|
|
330052
|
-
console.log(`${
|
|
330346
|
+
console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
|
|
330347
|
+
console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
|
|
330053
330348
|
log.info(`Server: ${serverUrl}`);
|
|
330054
330349
|
log.info(`Project: ${projectId}`);
|
|
330055
330350
|
log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
|
|
330056
330351
|
log.info(`Interval: ${pollIntervalMs}ms`);
|
|
330057
330352
|
if (process.env.DISCOVERY_PROMPT_V3 === "true") {
|
|
330058
|
-
log.ok("Discovery: V3
|
|
330353
|
+
log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
|
|
330059
330354
|
} else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
|
|
330060
|
-
log.ok("Discovery: V2.5
|
|
330355
|
+
log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
|
|
330061
330356
|
} else {
|
|
330062
|
-
log.ok("Discovery:
|
|
330357
|
+
log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
|
|
330063
330358
|
}
|
|
330064
330359
|
if ((0, import_runner_core5.hasUsableChromeProfile)()) {
|
|
330065
330360
|
log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
|
|
@@ -330071,8 +330366,8 @@ async function startRunner(opts) {
|
|
|
330071
330366
|
const devices = discoverAllDevices();
|
|
330072
330367
|
const capabilities = {
|
|
330073
330368
|
web: true,
|
|
330074
|
-
ios: devices.some((d) => d.platform === "IOS"),
|
|
330075
|
-
android: devices.some((d) => d.platform === "ANDROID")
|
|
330369
|
+
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|
|
330370
|
+
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
330076
330371
|
};
|
|
330077
330372
|
log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
|
|
330078
330373
|
try {
|
|
@@ -330133,6 +330428,33 @@ async function startRunner(opts) {
|
|
|
330133
330428
|
const startedAt = Date.now();
|
|
330134
330429
|
const MAX_BACKOFF_MS = 6e4;
|
|
330135
330430
|
let consecutivePollErrors = 0;
|
|
330431
|
+
let cachedDoctor = null;
|
|
330432
|
+
const DOCTOR_TTL_MS = 5 * 60 * 1e3;
|
|
330433
|
+
let doctorRefreshInFlight = false;
|
|
330434
|
+
const refreshDoctorInBackground = () => {
|
|
330435
|
+
if (doctorRefreshInFlight) return;
|
|
330436
|
+
doctorRefreshInFlight = true;
|
|
330437
|
+
setImmediate(() => {
|
|
330438
|
+
try {
|
|
330439
|
+
const report = runDoctor();
|
|
330440
|
+
cachedDoctor = { report, at: Date.now() };
|
|
330441
|
+
} catch (err) {
|
|
330442
|
+
log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
330443
|
+
} finally {
|
|
330444
|
+
doctorRefreshInFlight = false;
|
|
330445
|
+
}
|
|
330446
|
+
});
|
|
330447
|
+
};
|
|
330448
|
+
const getCachedDoctor = () => {
|
|
330449
|
+
if (!enableMobile) return void 0;
|
|
330450
|
+
if (!cachedDoctor) {
|
|
330451
|
+
refreshDoctorInBackground();
|
|
330452
|
+
return void 0;
|
|
330453
|
+
}
|
|
330454
|
+
const now = Date.now();
|
|
330455
|
+
if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
|
|
330456
|
+
return cachedDoctor.report;
|
|
330457
|
+
};
|
|
330136
330458
|
log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
|
|
330137
330459
|
const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
|
|
330138
330460
|
if (poolSize > 0) {
|
|
@@ -330241,7 +330563,11 @@ async function startRunner(opts) {
|
|
|
330241
330563
|
const sendHeartbeat = async () => {
|
|
330242
330564
|
try {
|
|
330243
330565
|
const devices = enableMobile ? discoverAllDevices() : [];
|
|
330244
|
-
const capabilities = enableMobile ? {
|
|
330566
|
+
const capabilities = enableMobile ? {
|
|
330567
|
+
web: true,
|
|
330568
|
+
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|
|
330569
|
+
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
330570
|
+
} : { web: true, ios: false, android: false };
|
|
330245
330571
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
330246
330572
|
const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
330247
330573
|
const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
|
|
@@ -330272,7 +330598,10 @@ async function startRunner(opts) {
|
|
|
330272
330598
|
resourceGovernor: resourceStatus.governor,
|
|
330273
330599
|
processInventory,
|
|
330274
330600
|
aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
|
|
330275
|
-
liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
|
|
330601
|
+
liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
330602
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
330603
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
330604
|
+
doctor: getCachedDoctor()
|
|
330276
330605
|
}),
|
|
330277
330606
|
signal: AbortSignal.timeout(1e4)
|
|
330278
330607
|
});
|
|
@@ -330472,219 +330801,6 @@ async function startRunner(opts) {
|
|
|
330472
330801
|
}
|
|
330473
330802
|
}
|
|
330474
330803
|
|
|
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
330804
|
// src/cli.ts
|
|
330689
330805
|
var pkg = require_package4();
|
|
330690
330806
|
var program = new import_commander.Command();
|