@wdio/browserstack-service 9.27.1 → 9.28.0

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/build/cleanup.js CHANGED
@@ -1,14 +1,13 @@
1
1
  // src/util.ts
2
2
  import { hostname as hostname2, platform as platform3, type as type2, version as version2, arch as arch3, tmpdir } from "node:os";
3
3
  import crypto from "node:crypto";
4
- import fs7 from "node:fs";
4
+ import fs8 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
  import { format, promisify as promisify2 } from "node:util";
7
- import path6 from "node:path";
7
+ import path7 from "node:path";
8
8
  import util3 from "node:util";
9
9
  import gitRepoInfo from "git-repo-info";
10
10
  import gitconfig from "gitconfiglocal";
11
- import { FormData } from "formdata-node";
12
11
  import { performance as performance2 } from "node:perf_hooks";
13
12
 
14
13
  // src/logPatcher.ts
@@ -54,12 +53,12 @@ var logPatcher_default = logPatcher;
54
53
 
55
54
  // src/instrumentation/performance/performance-tester.ts
56
55
  import { createObjectCsvWriter } from "csv-writer";
57
- import fs4 from "node:fs";
56
+ import fs5 from "node:fs";
58
57
  import fsPromise from "node:fs/promises";
59
58
  import { performance, PerformanceObserver } from "node:perf_hooks";
60
59
  import util2 from "node:util";
61
60
  import worker from "node:worker_threads";
62
- import path4 from "node:path";
61
+ import path5 from "node:path";
63
62
  import { arch as arch2, hostname, platform as platform2, type, version } from "node:os";
64
63
 
65
64
  // src/bstackLogger.ts
@@ -71,7 +70,7 @@ import logger from "@wdio/logger";
71
70
  // package.json
72
71
  var package_default = {
73
72
  name: "@wdio/browserstack-service",
74
- version: "9.27.0",
73
+ version: "9.27.2",
75
74
  description: "WebdriverIO service for better Browserstack integration",
76
75
  author: "Adam Bjerstedt <abjerstedt@gmail.com>",
77
76
  homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-browserstack-service",
@@ -118,7 +117,6 @@ var package_default = {
118
117
  "browserstack-local": "^1.5.1",
119
118
  chalk: "^5.3.0",
120
119
  "csv-writer": "^1.6.0",
121
- "formdata-node": "5.0.1",
122
120
  "git-repo-info": "^2.1.1",
123
121
  gitconfiglocal: "^2.1.0",
124
122
  glob: "^11.0.0",
@@ -149,6 +147,7 @@ var consoleHolder = Object.assign({}, console);
149
147
  var APP_ALLY_ISSUES_ENDPOINT = "api/v1/issues";
150
148
  var APP_ALLY_ISSUES_SUMMARY_ENDPOINT = "api/v1/issues-summary";
151
149
  var BSTACK_SERVICE_VERSION = bstackServiceVersion;
150
+ var BROWSERSTACK_TEST_PLAN_ID = "BROWSERSTACK_TEST_PLAN_ID";
152
151
  var LOGS_FILE = "logs/bstack-wdio-service.log";
153
152
  var SMART_SELECTION_MODE_RELEVANT_FIRST = "relevantFirst";
154
153
  var SMART_SELECTION_MODE_RELEVANT_ONLY = "relevantOnly";
@@ -162,6 +161,7 @@ var BROWSERSTACK_ACCESSIBILITY = "BROWSERSTACK_ACCESSIBILITY";
162
161
  var BROWSERSTACK_OBSERVABILITY = "BROWSERSTACK_OBSERVABILITY";
163
162
  var MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024;
164
163
  var GIT_META_DATA_TRUNCATED = "...[TRUNCATED]";
164
+ var BINARY_BUSY_ERROR_CODES = ["ETXTBSY", "EBUSY"];
165
165
  var WDIO_NAMING_PREFIX = "WebdriverIO-";
166
166
  var UPDATED_CLI_ENDPOINT = "sdk/v1/update_cli";
167
167
 
@@ -266,10 +266,10 @@ var APIUtils = class {
266
266
  };
267
267
 
268
268
  // src/cli/cliUtils.ts
269
- import fs3 from "node:fs";
269
+ import fs4 from "node:fs";
270
270
  import fsp from "node:fs/promises";
271
271
  import { platform, arch, homedir } from "node:os";
272
- import path3 from "node:path";
272
+ import path4 from "node:path";
273
273
  import util, { promisify } from "node:util";
274
274
  import { exec } from "node:child_process";
275
275
  import { Readable } from "node:stream";
@@ -277,7 +277,20 @@ import yauzl from "yauzl";
277
277
  import { threadId } from "node:worker_threads";
278
278
 
279
279
  // src/fetchWrapper.ts
280
- import { fetch as undiciFetch, ProxyAgent } from "undici";
280
+ import { fetch as undiciFetch, ProxyAgent, Agent as Agent2 } from "undici";
281
+
282
+ // src/caCert.ts
283
+ import { setGlobalDispatcher, Agent } from "undici";
284
+ import tls from "node:tls";
285
+ import fs2 from "node:fs";
286
+ import os from "node:os";
287
+ import path2 from "node:path";
288
+ var mergedCa;
289
+ function getMergedCa() {
290
+ return mergedCa;
291
+ }
292
+
293
+ // src/fetchWrapper.ts
281
294
  var ResponseError = class extends Error {
282
295
  response;
283
296
  constructor(message, res) {
@@ -285,26 +298,56 @@ var ResponseError = class extends Error {
285
298
  this.response = res;
286
299
  }
287
300
  };
288
- async function fetchWrap(input, init) {
289
- const res = await _fetch(input, init);
301
+ var dispatcherCache = /* @__PURE__ */ new Map();
302
+ function getDispatcher(proxyUrl, connectTimeoutMs) {
303
+ const ca = getMergedCa();
304
+ const key = `${proxyUrl ?? "direct"}:${connectTimeoutMs ?? "default"}:${ca ? "ca" : "noca"}`;
305
+ let dispatcher = dispatcherCache.get(key);
306
+ if (!dispatcher) {
307
+ const connect = {};
308
+ if (connectTimeoutMs) {
309
+ connect.timeout = connectTimeoutMs;
310
+ }
311
+ if (proxyUrl) {
312
+ dispatcher = new ProxyAgent({ uri: proxyUrl, connect, ...ca ? { requestTls: { ca } } : {} });
313
+ } else {
314
+ if (ca) {
315
+ connect.ca = ca;
316
+ }
317
+ dispatcher = new Agent2({ connect });
318
+ }
319
+ dispatcherCache.set(key, dispatcher);
320
+ }
321
+ return dispatcher;
322
+ }
323
+ async function fetchWrap(input, init, options) {
324
+ const res = await _fetch(input, init, options);
290
325
  if (!res.ok) {
291
326
  throw new ResponseError(`Error response from server ${res.status}: ${await res.text()}`, res);
292
327
  }
293
328
  return res;
294
329
  }
295
- function _fetch(input, init) {
330
+ function _fetch(input, init, options) {
331
+ const connectTimeoutMs = options?.connectTimeoutMs;
296
332
  const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
297
333
  if (proxyUrl) {
298
334
  const noProxy = process.env.NO_PROXY && process.env.NO_PROXY.trim() ? process.env.NO_PROXY.trim().split(/[\s,;]+/) : [];
299
335
  const request = new Request(input);
300
336
  const url = new URL(request.url);
301
337
  if (!noProxy.some((str) => url.hostname.endsWith(str))) {
338
+ const dispatcher = getDispatcher(proxyUrl, connectTimeoutMs);
302
339
  return undiciFetch(
303
340
  request.url,
304
- { ...init, dispatcher: new ProxyAgent(proxyUrl) }
341
+ { ...init, dispatcher }
305
342
  );
306
343
  }
307
344
  }
345
+ if (connectTimeoutMs) {
346
+ return undiciFetch(
347
+ new Request(input).url,
348
+ { ...init, dispatcher: getDispatcher(void 0, connectTimeoutMs) }
349
+ );
350
+ }
308
351
  return fetch(input, init);
309
352
  }
310
353
 
@@ -456,20 +499,20 @@ var DISPATCHER_EVENTS = {
456
499
  };
457
500
 
458
501
  // src/cli/cliLogger.ts
459
- import path2 from "node:path";
460
- import fs2 from "node:fs";
502
+ import path3 from "node:path";
503
+ import fs3 from "node:fs";
461
504
  import chalk2 from "chalk";
462
505
  import logger2 from "@wdio/logger";
463
506
  var log2 = logger2("@wdio/browserstack-service/cli");
464
507
  var BStackLogger2 = class {
465
- static logFilePath = path2.join(process.cwd(), LOGS_FILE);
466
- static logFolderPath = path2.join(process.cwd(), "logs");
508
+ static logFilePath = path3.join(process.cwd(), LOGS_FILE);
509
+ static logFolderPath = path3.join(process.cwd(), "logs");
467
510
  static logFileStream;
468
511
  static logToFile(logMessage, logLevel) {
469
512
  try {
470
513
  if (!this.logFileStream) {
471
514
  this.ensureLogsFolder();
472
- this.logFileStream = fs2.createWriteStream(this.logFilePath, { flags: "a" });
515
+ this.logFileStream = fs3.createWriteStream(this.logFilePath, { flags: "a" });
473
516
  }
474
517
  if (this.logFileStream && this.logFileStream.writable) {
475
518
  this.logFileStream.write(this.formatLog(logMessage, logLevel));
@@ -513,13 +556,13 @@ var BStackLogger2 = class {
513
556
  this.logFileStream = null;
514
557
  }
515
558
  static clearLogFile() {
516
- if (fs2.existsSync(this.logFilePath)) {
517
- fs2.truncateSync(this.logFilePath);
559
+ if (fs3.existsSync(this.logFilePath)) {
560
+ fs3.truncateSync(this.logFilePath);
518
561
  }
519
562
  }
520
563
  static ensureLogsFolder() {
521
- if (!fs2.existsSync(this.logFolderPath)) {
522
- fs2.mkdirSync(this.logFolderPath);
564
+ if (!fs3.existsSync(this.logFolderPath)) {
565
+ fs3.mkdirSync(this.logFolderPath);
523
566
  }
524
567
  }
525
568
  };
@@ -574,6 +617,9 @@ var CLI_LOCK_POLL_MS = 1e3;
574
617
  var CLI_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
575
618
  var CLI_DOWNLOAD_TMP_PREFIX = "downloaded_file_";
576
619
  var CLI_DOWNLOAD_TMP_SUFFIX = ".zip";
620
+ var CLI_CONNECT_TIMEOUT_MS = Number(process.env.BROWSERSTACK_CLI_CONNECT_TIMEOUT_MS) || 60 * 1e3;
621
+ var CLI_FETCH_MAX_ATTEMPTS = 3;
622
+ var CLI_FETCH_RETRY_BASE_DELAY_MS = 500;
577
623
  var CLIUtils = class _CLIUtils {
578
624
  static automationFrameworkDetail = {};
579
625
  static testFrameworkDetail = {};
@@ -598,6 +644,7 @@ var CLIUtils = class _CLIUtils {
598
644
  modifiedOpts.browserStackLocalOptions = modifiedOpts.opts;
599
645
  delete modifiedOpts.opts;
600
646
  }
647
+ delete modifiedOpts.testManagementOptions;
601
648
  modifiedOpts.testContextOptions = {
602
649
  skipSessionName: isFalse(modifiedOpts.setSessionName),
603
650
  skipSessionStatus: isFalse(modifiedOpts.setSessionStatus),
@@ -616,6 +663,8 @@ var CLIUtils = class _CLIUtils {
616
663
  options.testObservability
617
664
  );
618
665
  const observabilityOptions = options.testObservabilityOptions || {};
666
+ const testManagementOptions = options.testManagementOptions || {};
667
+ const testPlanId = typeof testManagementOptions.testPlanId === "string" ? testManagementOptions.testPlanId.trim() : "";
619
668
  const binconfig = {
620
669
  userName: observabilityOptions.user || config.user,
621
670
  accessKey: observabilityOptions.key || config.key,
@@ -627,26 +676,26 @@ var CLIUtils = class _CLIUtils {
627
676
  binconfig.buildName = observabilityOptions.buildName || binconfig.buildName;
628
677
  binconfig.projectName = observabilityOptions.projectName || binconfig.projectName;
629
678
  binconfig.buildTag = this.getObservabilityBuildTags(observabilityOptions, buildTag) || [];
630
- let caps = capabilities;
631
- if (capabilities && !Array.isArray(capabilities)) {
632
- caps = [capabilities];
679
+ if (testPlanId.length > 0) {
680
+ binconfig.testManagementOptions = {
681
+ testPlanId
682
+ };
633
683
  }
634
- if (Array.isArray(caps)) {
635
- for (const cap of caps) {
636
- const platform4 = {};
637
- const capability = cap;
638
- Object.keys(capability).filter((key) => key !== "bstack:options").forEach((key) => {
639
- platform4[key] = capability[key];
684
+ const caps = Array.isArray(capabilities) ? capabilities : [capabilities];
685
+ for (const cap of caps) {
686
+ const platform4 = {};
687
+ const capability = cap;
688
+ Object.keys(capability).filter((key) => key !== "bstack:options").forEach((key) => {
689
+ platform4[key] = capability[key];
690
+ });
691
+ if (capability["bstack:options"]) {
692
+ Object.keys(
693
+ capability["bstack:options"]
694
+ ).forEach((key) => {
695
+ platform4[key] = capability["bstack:options"][key];
640
696
  });
641
- if (capability["bstack:options"]) {
642
- Object.keys(
643
- capability["bstack:options"]
644
- ).forEach((key) => {
645
- platform4[key] = capability["bstack:options"][key];
646
- });
647
- }
648
- binconfig.platforms.push(platform4);
649
697
  }
698
+ binconfig.platforms.push(platform4);
650
699
  }
651
700
  return JSON.stringify(binconfig);
652
701
  }
@@ -677,7 +726,7 @@ var CLIUtils = class _CLIUtils {
677
726
  BStackLogger2.debug(`Resolved binary path: ${finalBinaryPath}`);
678
727
  return finalBinaryPath;
679
728
  } catch (err) {
680
- BStackLogger2.debug(
729
+ BStackLogger2.warn(
681
730
  `Error in setting up cli path directory, Exception: ${util.format(err)}`
682
731
  );
683
732
  }
@@ -688,7 +737,7 @@ var CLIUtils = class _CLIUtils {
688
737
  BStackLogger2.debug(
689
738
  `Worker process detected, skipping CLI update. Using existing: ${existingCliPath}`
690
739
  );
691
- if (existingCliPath && fs3.existsSync(existingCliPath)) {
740
+ if (existingCliPath && fs4.existsSync(existingCliPath)) {
692
741
  return existingCliPath;
693
742
  }
694
743
  BStackLogger2.warn(
@@ -705,9 +754,20 @@ var CLIUtils = class _CLIUtils {
705
754
  sdk_language: this.getSdkLanguage()
706
755
  };
707
756
  if (!isNullOrEmpty(existingCliPath)) {
708
- queryParams.cli_version = await this.runShellCommand(
757
+ if (this.isBinaryBusy(existingCliPath)) {
758
+ BStackLogger2.warn(`Existing binary is currently in use, skipping update: ${existingCliPath}`);
759
+ PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE);
760
+ return existingCliPath;
761
+ }
762
+ const version3 = await this.runShellCommand(
709
763
  `${existingCliPath} version`
710
764
  );
765
+ if (version3.toLowerCase().includes("text file busy")) {
766
+ BStackLogger2.warn(`Binary busy during version check, skipping update: ${existingCliPath}`);
767
+ PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE);
768
+ return existingCliPath;
769
+ }
770
+ queryParams.cli_version = version3;
711
771
  }
712
772
  const response = await this.requestToUpdateCLI(queryParams, config);
713
773
  if (nestedKeyValue(response, ["updated_cli_version"])) {
@@ -737,8 +797,8 @@ var CLIUtils = class _CLIUtils {
737
797
  if (isNullOrEmpty(writableDir)) {
738
798
  throw new Error("No writable directory available for the CLI");
739
799
  }
740
- const cliDirPath = path3.join(writableDir, "cli");
741
- if (!fs3.existsSync(cliDirPath)) {
800
+ const cliDirPath = path4.join(writableDir, "cli");
801
+ if (!fs4.existsSync(cliDirPath)) {
742
802
  createDir(cliDirPath);
743
803
  }
744
804
  return cliDirPath;
@@ -752,37 +812,37 @@ var CLIUtils = class _CLIUtils {
752
812
  static getWritableDir() {
753
813
  const writableDirOptions = [
754
814
  process.env.BROWSERSTACK_FILES_DIR,
755
- path3.join(homedir(), ".browserstack"),
756
- path3.join("tmp", ".browserstack")
815
+ path4.join(homedir(), ".browserstack"),
816
+ path4.join("tmp", ".browserstack")
757
817
  ];
758
- for (const path9 of writableDirOptions) {
759
- if (isNullOrEmpty(path9)) {
818
+ for (const path10 of writableDirOptions) {
819
+ if (isNullOrEmpty(path10)) {
760
820
  continue;
761
821
  }
762
822
  try {
763
- if (fs3.existsSync(path9)) {
764
- BStackLogger2.debug(`File ${path9} already exist`);
765
- if (!isWritable(path9)) {
766
- BStackLogger2.debug(`Giving write permission to ${path9}`);
767
- const success = setReadWriteAccess(path9);
823
+ if (fs4.existsSync(path10)) {
824
+ BStackLogger2.debug(`File ${path10} already exist`);
825
+ if (!isWritable(path10)) {
826
+ BStackLogger2.debug(`Giving write permission to ${path10}`);
827
+ const success = setReadWriteAccess(path10);
768
828
  if (!isTrue(success)) {
769
829
  BStackLogger2.warn(
770
- `Unable to provide write permission to ${path9}`
830
+ `Unable to provide write permission to ${path10}`
771
831
  );
772
832
  }
773
833
  }
774
834
  } else {
775
- BStackLogger2.debug(`File does not exist: ${path9}`);
776
- createDir(path9);
777
- BStackLogger2.debug(`Giving write permission to ${path9}`);
778
- const success = setReadWriteAccess(path9);
835
+ BStackLogger2.debug(`File does not exist: ${path10}`);
836
+ createDir(path10);
837
+ BStackLogger2.debug(`Giving write permission to ${path10}`);
838
+ const success = setReadWriteAccess(path10);
779
839
  if (!isTrue(success)) {
780
840
  BStackLogger2.warn(
781
- `Unable to provide write permission to ${path9}`
841
+ `Unable to provide write permission to ${path10}`
782
842
  );
783
843
  }
784
844
  }
785
- return path9;
845
+ return path10;
786
846
  } catch (err) {
787
847
  BStackLogger2.error(
788
848
  `Unable to get writable directory, exception ${util.format(err)}`
@@ -793,16 +853,16 @@ var CLIUtils = class _CLIUtils {
793
853
  }
794
854
  static getExistingCliPath(cliDir) {
795
855
  try {
796
- if (!fs3.existsSync(cliDir) || !fs3.statSync(cliDir).isDirectory()) {
856
+ if (!fs4.existsSync(cliDir) || !fs4.statSync(cliDir).isDirectory()) {
797
857
  return "";
798
858
  }
799
- const allBinaries = fs3.readdirSync(cliDir).map((file) => path3.join(cliDir, file)).filter(
800
- (filePath) => fs3.statSync(filePath).isFile() && path3.basename(filePath).startsWith("binary-")
859
+ const allBinaries = fs4.readdirSync(cliDir).map((file) => path4.join(cliDir, file)).filter(
860
+ (filePath) => fs4.statSync(filePath).isFile() && path4.basename(filePath).startsWith("binary-")
801
861
  );
802
862
  if (allBinaries.length > 0) {
803
863
  const latestBinary = allBinaries.map((filePath) => ({
804
864
  filePath,
805
- mtime: fs3.statSync(filePath).mtime
865
+ mtime: fs4.statSync(filePath).mtime
806
866
  })).reduce(
807
867
  (latest, current) => {
808
868
  if (!latest || !latest.mtime) {
@@ -823,6 +883,58 @@ var CLIUtils = class _CLIUtils {
823
883
  return "";
824
884
  }
825
885
  }
886
+ static isBinaryBusy(binaryPath) {
887
+ if (isNullOrEmpty(binaryPath)) {
888
+ return false;
889
+ }
890
+ if (platform() === "darwin") {
891
+ return false;
892
+ }
893
+ if (!fs4.existsSync(binaryPath)) {
894
+ return false;
895
+ }
896
+ try {
897
+ const fd = fs4.openSync(binaryPath, "r+");
898
+ fs4.closeSync(fd);
899
+ return false;
900
+ } catch (err) {
901
+ if (BINARY_BUSY_ERROR_CODES.includes(err.code)) {
902
+ BStackLogger2.debug(`Binary is busy: ${binaryPath}`);
903
+ return true;
904
+ }
905
+ BStackLogger2.debug(`Error checking if binary is busy: ${err.message}`);
906
+ return false;
907
+ }
908
+ }
909
+ /**
910
+ * fetch wrapper for CLI network calls that (a) raises undici's connect timeout
911
+ * above the non-overridable 10s default of global fetch and (b) retries a few
912
+ * times with linear backoff on transient connection failures. Both are needed
913
+ * for reliability in constrained CI/containerised networks (SDK-6152).
914
+ */
915
+ static fetchWithRetry = async (input, init, label) => {
916
+ let lastError;
917
+ for (let attempt = 1; attempt <= CLI_FETCH_MAX_ATTEMPTS; attempt++) {
918
+ try {
919
+ return await _fetch(input, init, { connectTimeoutMs: CLI_CONNECT_TIMEOUT_MS });
920
+ } catch (err) {
921
+ lastError = err;
922
+ const reason = (
923
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
924
+ err?.cause?.code || err?.code || err?.message
925
+ );
926
+ BStackLogger2.warn(
927
+ `${label} attempt ${attempt}/${CLI_FETCH_MAX_ATTEMPTS} failed: ${reason}`
928
+ );
929
+ if (attempt < CLI_FETCH_MAX_ATTEMPTS) {
930
+ await new Promise(
931
+ (resolve) => setTimeout(resolve, CLI_FETCH_RETRY_BASE_DELAY_MS * attempt)
932
+ );
933
+ }
934
+ }
935
+ }
936
+ throw lastError;
937
+ };
826
938
  static requestToUpdateCLI = async (queryParams, config) => {
827
939
  const params = new URLSearchParams(queryParams);
828
940
  const requestInit = {
@@ -831,9 +943,10 @@ var CLIUtils = class _CLIUtils {
831
943
  Authorization: `Basic ${Buffer.from(`${getBrowserStackUser(config)}:${getBrowserStackKey(config)}`).toString("base64")}`
832
944
  }
833
945
  };
834
- const response = await _fetch(
946
+ const response = await this.fetchWithRetry(
835
947
  `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/${UPDATED_CLI_ENDPOINT}?${params.toString()}`,
836
- requestInit
948
+ requestInit,
949
+ "update_cli request"
837
950
  );
838
951
  const jsonResponse = await response.json();
839
952
  BStackLogger2.debug(`response ${JSON.stringify(jsonResponse)}`);
@@ -858,11 +971,11 @@ var CLIUtils = class _CLIUtils {
858
971
  });
859
972
  }
860
973
  static downloadLatestBinary = async (binDownloadUrl, cliDir) => {
861
- const lockPath = path3.join(cliDir, "download.lock");
974
+ const lockPath = path4.join(cliDir, "download.lock");
862
975
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
863
976
  const parseLockFile = () => {
864
977
  try {
865
- const content = fs3.readFileSync(lockPath, "utf8").trim();
978
+ const content = fs4.readFileSync(lockPath, "utf8").trim();
866
979
  const [pidLine, timestampLine] = content.split("\n");
867
980
  const pid = Number.parseInt(pidLine, 10);
868
981
  const timestamp = Number.parseInt(timestampLine, 10);
@@ -886,20 +999,20 @@ var CLIUtils = class _CLIUtils {
886
999
  const start = Date.now();
887
1000
  while (true) {
888
1001
  try {
889
- const fd = fs3.openSync(lockPath, "wx");
1002
+ const fd = fs4.openSync(lockPath, "wx");
890
1003
  try {
891
- fs3.writeFileSync(fd, `${process.pid}
1004
+ fs4.writeFileSync(fd, `${process.pid}
892
1005
  ${Date.now()}
893
1006
  `);
894
1007
  } catch {
895
1008
  }
896
1009
  return () => {
897
1010
  try {
898
- fs3.closeSync(fd);
1011
+ fs4.closeSync(fd);
899
1012
  } catch {
900
1013
  }
901
1014
  try {
902
- fs3.unlinkSync(lockPath);
1015
+ fs4.unlinkSync(lockPath);
903
1016
  } catch {
904
1017
  }
905
1018
  };
@@ -915,14 +1028,14 @@ ${Date.now()}
915
1028
  `Stale CLI download lock detected (pid=${lockMeta.pid}, age=${lockAge}ms). Removing lock.`
916
1029
  );
917
1030
  try {
918
- fs3.unlinkSync(lockPath);
1031
+ fs4.unlinkSync(lockPath);
919
1032
  } catch {
920
1033
  }
921
1034
  continue;
922
1035
  }
923
1036
  }
924
1037
  const existingBinary = _CLIUtils.getExistingCliPath(cliDir);
925
- if (existingBinary && fs3.existsSync(existingBinary) && fs3.statSync(existingBinary).size > 0) {
1038
+ if (existingBinary && fs4.existsSync(existingBinary) && fs4.statSync(existingBinary).size > 0) {
926
1039
  BStackLogger2.debug(
927
1040
  `Binary appeared while waiting for lock: ${existingBinary}`
928
1041
  );
@@ -943,14 +1056,17 @@ ${Date.now()}
943
1056
  const cleanupTemporaryDownloads = (maxAgeMs = CLI_LOCK_TIMEOUT_MS) => {
944
1057
  try {
945
1058
  const now = Date.now();
946
- for (const entry of fs3.readdirSync(cliDir)) {
947
- if (!entry.startsWith(CLI_DOWNLOAD_TMP_PREFIX) || !entry.endsWith(CLI_DOWNLOAD_TMP_SUFFIX)) {
1059
+ const tmpExtractRe = /\.tmp\.\d+$/;
1060
+ for (const entry of fs4.readdirSync(cliDir)) {
1061
+ const isDownloadZip = entry.startsWith(CLI_DOWNLOAD_TMP_PREFIX) && entry.endsWith(CLI_DOWNLOAD_TMP_SUFFIX);
1062
+ const isExtractTmp = tmpExtractRe.test(entry);
1063
+ if (!isDownloadZip && !isExtractTmp) {
948
1064
  continue;
949
1065
  }
950
- const filePath = path3.join(cliDir, entry);
1066
+ const filePath = path4.join(cliDir, entry);
951
1067
  let stats;
952
1068
  try {
953
- stats = fs3.statSync(filePath);
1069
+ stats = fs4.statSync(filePath);
954
1070
  } catch {
955
1071
  continue;
956
1072
  }
@@ -958,16 +1074,16 @@ ${Date.now()}
958
1074
  continue;
959
1075
  }
960
1076
  try {
961
- fs3.unlinkSync(filePath);
1077
+ fs4.unlinkSync(filePath);
962
1078
  } catch (err) {
963
1079
  BStackLogger2.debug(
964
- `Failed to delete temp CLI zip file ${filePath}: ${util.format(err)}`
1080
+ `Failed to delete temp CLI file ${filePath}: ${util.format(err)}`
965
1081
  );
966
1082
  }
967
1083
  }
968
1084
  } catch (err) {
969
1085
  BStackLogger2.debug(
970
- `Failed to scan temp CLI downloads in ${cliDir}: ${util.format(err)}`
1086
+ `Failed to scan temp CLI files in ${cliDir}: ${util.format(err)}`
971
1087
  );
972
1088
  }
973
1089
  };
@@ -998,7 +1114,7 @@ ${Date.now()}
998
1114
  }
999
1115
  releaseLock = lockResult;
1000
1116
  const existingBinary = _CLIUtils.getExistingCliPath(cliDir);
1001
- if (existingBinary && fs3.existsSync(existingBinary) && fs3.statSync(existingBinary).size > 0) {
1117
+ if (existingBinary && fs4.existsSync(existingBinary) && fs4.statSync(existingBinary).size > 0) {
1002
1118
  BStackLogger2.debug(
1003
1119
  `Binary already exists after acquiring lock: ${existingBinary}`
1004
1120
  );
@@ -1007,13 +1123,12 @@ ${Date.now()}
1007
1123
  return existingBinary;
1008
1124
  }
1009
1125
  cleanupTemporaryDownloads();
1010
- const zipFilePath = path3.join(
1126
+ const zipFilePath = path4.join(
1011
1127
  cliDir,
1012
1128
  `${CLI_DOWNLOAD_TMP_PREFIX}${process.pid}_${Date.now()}${CLI_DOWNLOAD_TMP_SUFFIX}`
1013
1129
  );
1014
- const downloadedFileStream = fs3.createWriteStream(zipFilePath);
1130
+ const downloadedFileStream = fs4.createWriteStream(zipFilePath);
1015
1131
  return new Promise((resolve, reject) => {
1016
- const binaryName = null;
1017
1132
  const processDownload = async () => {
1018
1133
  const abortController = new AbortController();
1019
1134
  const timeout = setTimeout(
@@ -1022,9 +1137,11 @@ ${Date.now()}
1022
1137
  );
1023
1138
  let response;
1024
1139
  try {
1025
- response = await _fetch(binDownloadUrl, {
1026
- signal: abortController.signal
1027
- });
1140
+ response = await _fetch(
1141
+ binDownloadUrl,
1142
+ { signal: abortController.signal },
1143
+ { connectTimeoutMs: CLI_CONNECT_TIMEOUT_MS }
1144
+ );
1028
1145
  } finally {
1029
1146
  clearTimeout(timeout);
1030
1147
  }
@@ -1047,7 +1164,6 @@ ${Date.now()}
1047
1164
  nodeStream.pipe(downloadedFileStream);
1048
1165
  _CLIUtils.downloadFileStream(
1049
1166
  downloadedFileStream,
1050
- binaryName,
1051
1167
  zipFilePath,
1052
1168
  cliDir,
1053
1169
  (result) => {
@@ -1081,40 +1197,104 @@ ${Date.now()}
1081
1197
  return null;
1082
1198
  }
1083
1199
  };
1084
- static downloadFileStream(downloadedFileStream, binaryName, zipFilePath, cliDir, resolve, reject) {
1200
+ static downloadFileStream(downloadedFileStream, zipFilePath, cliDir, resolve, reject) {
1085
1201
  downloadedFileStream.on("close", async function() {
1086
1202
  const yauzlOpenPromise = promisify(yauzl.open);
1087
1203
  try {
1088
1204
  const zipfile = await yauzlOpenPromise(zipFilePath, {
1089
1205
  lazyEntries: true
1090
1206
  });
1207
+ let resolvedBinaryPath = null;
1091
1208
  zipfile.readEntry();
1092
1209
  zipfile.on("entry", async (entry) => {
1093
- if (!binaryName) {
1094
- binaryName = entry.fileName;
1095
- }
1096
1210
  if (/\/$/.test(entry.fileName)) {
1097
1211
  zipfile.readEntry();
1098
- } else {
1099
- const writeStream = fs3.createWriteStream(
1100
- path3.join(cliDir, entry.fileName)
1101
- );
1102
- const openReadStreamPromise = promisify(
1212
+ return;
1213
+ }
1214
+ const candidatePath = path4.join(cliDir, entry.fileName);
1215
+ const resolvedCandidate = path4.resolve(candidatePath);
1216
+ const resolvedDir = path4.resolve(cliDir) + path4.sep;
1217
+ if (!resolvedCandidate.startsWith(resolvedDir)) {
1218
+ zipfile.close();
1219
+ reject(new Error(`Zip-slip detected: entry "${entry.fileName}" resolves outside ${cliDir}`));
1220
+ return;
1221
+ }
1222
+ const isBinaryEntry = path4.basename(entry.fileName).startsWith("binary-");
1223
+ if (!isBinaryEntry) {
1224
+ const directStream = fs4.createWriteStream(candidatePath);
1225
+ directStream.on("error", (writeErr) => {
1226
+ zipfile.close();
1227
+ reject(writeErr);
1228
+ });
1229
+ const openReadStreamPromise2 = promisify(
1103
1230
  zipfile.openReadStream
1104
1231
  ).bind(zipfile);
1105
1232
  try {
1106
- const readStream = await openReadStreamPromise(entry);
1233
+ const readStream = await openReadStreamPromise2(entry);
1107
1234
  readStream.on("end", function() {
1108
- writeStream.close();
1109
- zipfile.readEntry();
1235
+ directStream.end();
1236
+ directStream.on("close", () => zipfile.readEntry());
1110
1237
  });
1111
- readStream.pipe(writeStream);
1238
+ readStream.pipe(directStream);
1112
1239
  } catch (zipErr) {
1240
+ zipfile.close();
1113
1241
  reject(zipErr);
1114
1242
  }
1115
- if (entry.fileName === binaryName) {
1243
+ return;
1244
+ }
1245
+ const finalPath = candidatePath;
1246
+ const tempPath = path4.join(cliDir, `${entry.fileName}.tmp.${process.pid}`);
1247
+ const writeStream = fs4.createWriteStream(tempPath);
1248
+ let writeStreamErrored = false;
1249
+ writeStream.on("error", (writeErr) => {
1250
+ writeStreamErrored = true;
1251
+ fsp.unlink(tempPath).catch(() => {
1252
+ });
1253
+ zipfile.close();
1254
+ reject(writeErr);
1255
+ });
1256
+ writeStream.on("close", async () => {
1257
+ if (writeStreamErrored) {
1258
+ return;
1259
+ }
1260
+ try {
1261
+ await fsp.chmod(tempPath, "0755");
1262
+ try {
1263
+ await fsp.rename(tempPath, finalPath);
1264
+ } catch (renameErr) {
1265
+ if (renameErr.code !== "EXDEV") {
1266
+ throw renameErr;
1267
+ }
1268
+ BStackLogger2.warn(`Atomic rename failed (cross-device), falling back to copy: ${renameErr.message}`);
1269
+ await fsp.copyFile(tempPath, finalPath);
1270
+ await fsp.unlink(tempPath).catch(() => {
1271
+ });
1272
+ }
1273
+ if (!resolvedBinaryPath) {
1274
+ resolvedBinaryPath = finalPath;
1275
+ }
1276
+ zipfile.readEntry();
1277
+ } catch (err) {
1278
+ await fsp.unlink(tempPath).catch(() => {
1279
+ });
1116
1280
  zipfile.close();
1281
+ reject(err);
1117
1282
  }
1283
+ });
1284
+ const openReadStreamPromise = promisify(
1285
+ zipfile.openReadStream
1286
+ ).bind(zipfile);
1287
+ try {
1288
+ const readStream = await openReadStreamPromise(entry);
1289
+ readStream.on("end", function() {
1290
+ writeStream.end();
1291
+ });
1292
+ readStream.pipe(writeStream);
1293
+ } catch (zipErr) {
1294
+ fsp.unlink(tempPath).catch(() => {
1295
+ });
1296
+ zipfile.close();
1297
+ reject(zipErr);
1118
1298
  }
1119
1299
  });
1120
1300
  zipfile.on("error", (zipErr) => {
@@ -1122,16 +1302,15 @@ ${Date.now()}
1122
1302
  });
1123
1303
  zipfile.once("end", () => {
1124
1304
  fsp.unlink(zipFilePath).catch(() => {
1125
- BStackLogger2.warn(
1126
- `Failed to delete zip file: ${zipFilePath}`
1127
- );
1128
- });
1129
- fsp.chmod(`${cliDir}/${binaryName}`, "0755").then(() => {
1130
- resolve(`${cliDir}/${binaryName}`);
1131
- }).catch((err) => {
1132
- reject(err);
1305
+ BStackLogger2.warn(`Failed to delete zip file: ${zipFilePath}`);
1133
1306
  });
1307
+ if (!resolvedBinaryPath) {
1308
+ zipfile.close();
1309
+ reject(new Error("No binary-* entry found in zip; cannot complete CLI binary extraction"));
1310
+ return;
1311
+ }
1134
1312
  zipfile.close();
1313
+ resolve(resolvedBinaryPath);
1135
1314
  });
1136
1315
  } catch (err) {
1137
1316
  reject(err);
@@ -1246,11 +1425,11 @@ var PerformanceTester = class _PerformanceTester {
1246
1425
  static browser;
1247
1426
  static scenarioThatRan;
1248
1427
  static jsonReportDirName = "performance-report";
1249
- static jsonReportDirPath = path4.join(process.cwd(), "logs", this.jsonReportDirName);
1428
+ static jsonReportDirPath = path5.join(process.cwd(), "logs", this.jsonReportDirName);
1250
1429
  static jsonReportFileName = `${this.jsonReportDirPath}/performance-report-${_PerformanceTester.getProcessId()}.json`;
1251
1430
  static startMonitoring(csvName = "performance-report.csv") {
1252
- if (!fs4.existsSync(this.jsonReportDirPath)) {
1253
- fs4.mkdirSync(this.jsonReportDirPath, { recursive: true });
1431
+ if (!fs5.existsSync(this.jsonReportDirPath)) {
1432
+ fs5.mkdirSync(this.jsonReportDirPath, { recursive: true });
1254
1433
  }
1255
1434
  this._observer = new PerformanceObserver((list) => {
1256
1435
  list.getEntries().filter((entry) => entry.entryType === "measure").forEach(
@@ -1324,10 +1503,10 @@ var PerformanceTester = class _PerformanceTester {
1324
1503
  this.started = false;
1325
1504
  this.generateCSV(this._events);
1326
1505
  const content = this.generateReport(this._events);
1327
- const dir = path4.join(process.cwd(), filename);
1506
+ const dir = path5.join(process.cwd(), filename);
1328
1507
  try {
1329
1508
  await fsPromise.writeFile(dir, content);
1330
- BStackLogger.info(`Performance report is at ${path4}`);
1509
+ BStackLogger.info(`Performance report is at ${path5}`);
1331
1510
  } catch (err) {
1332
1511
  BStackLogger.error(`Error in writing html ${util2.format(err)}`);
1333
1512
  }
@@ -1468,6 +1647,7 @@ var PerformanceTester = class _PerformanceTester {
1468
1647
  static end(event, success = true, failure, details = {}) {
1469
1648
  performance.mark(event + "-end");
1470
1649
  performance.measure(event, event + "-start", event + "-end");
1650
+ delete this.eventsMap[event + "-start"];
1471
1651
  this.details[event] = Object.assign({ success, failure: util2.format(failure) }, Object.assign(Object.assign({
1472
1652
  clientWorkerId: _PerformanceTester.getClientWorkerId(),
1473
1653
  worker: _PerformanceTester.getProcessId(),
@@ -1497,7 +1677,7 @@ var PerformanceTester = class _PerformanceTester {
1497
1677
  this.start(EVENTS.SDK_KEY_METRICS_PREPARATION);
1498
1678
  let measures = [];
1499
1679
  if (await fsPromise.access(this.jsonReportDirPath).then(() => true).catch(() => false)) {
1500
- const files = (await fsPromise.readdir(this.jsonReportDirPath)).map((file) => path4.resolve(this.jsonReportDirPath, file));
1680
+ const files = (await fsPromise.readdir(this.jsonReportDirPath)).map((file) => path5.resolve(this.jsonReportDirPath, file));
1501
1681
  measures = (await Promise.all(files.map((file) => fsPromise.readFile(file, "utf-8")))).map((el) => `[${el.slice(0, -1)}]`).map((el) => JSON.parse(el)).flat();
1502
1682
  }
1503
1683
  BStackLogger.debug(`[Performance Upload] Total events from files: ${measures.length}`);
@@ -1574,7 +1754,7 @@ var PerformanceTester = class _PerformanceTester {
1574
1754
  if (await fsPromise.access(this.jsonReportDirPath).then(() => true, () => false)) {
1575
1755
  const files = await fsPromise.readdir(this.jsonReportDirPath);
1576
1756
  for (const file of files) {
1577
- await fsPromise.unlink(path4.join(this.jsonReportDirPath, file));
1757
+ await fsPromise.unlink(path5.join(this.jsonReportDirPath, file));
1578
1758
  }
1579
1759
  BStackLogger.debug(`[Performance Upload] Cleaned up ${files.length} temporary report files`);
1580
1760
  }
@@ -1619,17 +1799,18 @@ var logBuildError = (error, product = "") => {
1619
1799
  }
1620
1800
  };
1621
1801
  var getProductMapForBuildStartCall = (config, accessibilityAutomation) => {
1622
- return {
1623
- observability: config.testObservability.enabled,
1624
- accessibility: accessibilityAutomation,
1625
- percy: config.percy,
1626
- automate: config.automate,
1627
- app_automate: config.appAutomate
1628
- };
1802
+ const entries = [
1803
+ ["observability", config.testObservability.enabled],
1804
+ ["accessibility", accessibilityAutomation],
1805
+ ["percy", config.percy],
1806
+ ["automate", config.automate],
1807
+ ["app_automate", config.appAutomate]
1808
+ ];
1809
+ return Object.fromEntries(entries.filter(([, v]) => v !== null));
1629
1810
  };
1630
1811
 
1631
1812
  // src/testorchestration/testorcherstrationutils.ts
1632
- import fs5 from "node:fs";
1813
+ import fs6 from "node:fs";
1633
1814
  var RUN_SMART_SELECTION = "runSmartSelection";
1634
1815
  var ALLOWED_ORCHESTRATION_KEYS = [
1635
1816
  RUN_SMART_SELECTION
@@ -1859,13 +2040,13 @@ var OrchestrationUtils = class _OrchestrationUtils {
1859
2040
  * @returns Formatted list of repository configurations
1860
2041
  */
1861
2042
  _loadSourceFromFile(filePath) {
1862
- if (!fs5.existsSync(filePath)) {
2043
+ if (!fs6.existsSync(filePath)) {
1863
2044
  BStackLogger.error(`Source file '${filePath}' does not exist.`);
1864
2045
  return [];
1865
2046
  }
1866
2047
  let data = null;
1867
2048
  try {
1868
- const fileContent = fs5.readFileSync(filePath, "utf8");
2049
+ const fileContent = fs6.readFileSync(filePath, "utf8");
1869
2050
  data = JSON.parse(fileContent);
1870
2051
  } catch (error) {
1871
2052
  const message = error instanceof Error ? error.message : String(error);
@@ -2448,12 +2629,11 @@ var usageStats_default = UsageStats;
2448
2629
 
2449
2630
  // src/util.ts
2450
2631
  import { create } from "tar";
2451
- import { fileFromPath } from "formdata-node/file-from-path";
2452
2632
 
2453
2633
  // src/scripts/accessibility-scripts.ts
2454
- import path5 from "node:path";
2455
- import fs6 from "node:fs";
2456
- import os from "node:os";
2634
+ import path6 from "node:path";
2635
+ import fs7 from "node:fs";
2636
+ import os2 from "node:os";
2457
2637
  var AccessibilityScripts = class _AccessibilityScripts {
2458
2638
  static instance = null;
2459
2639
  performScan = null;
@@ -2467,7 +2647,7 @@ var AccessibilityScripts = class _AccessibilityScripts {
2467
2647
  // don't allow to create instances from it other than through `checkAndGetInstance`
2468
2648
  constructor() {
2469
2649
  this.browserstackFolderPath = this.getWritableDir();
2470
- this.commandsPath = path5.join(this.browserstackFolderPath, "commands.json");
2650
+ this.commandsPath = path6.join(this.browserstackFolderPath, "commands.json");
2471
2651
  }
2472
2652
  static checkAndGetInstance() {
2473
2653
  if (!_AccessibilityScripts.instance) {
@@ -2479,17 +2659,17 @@ var AccessibilityScripts = class _AccessibilityScripts {
2479
2659
  /* eslint-disable @typescript-eslint/no-unused-vars */
2480
2660
  getWritableDir() {
2481
2661
  const orderedPaths = [
2482
- path5.join(os.homedir(), ".browserstack"),
2662
+ path6.join(os2.homedir(), ".browserstack"),
2483
2663
  process.cwd(),
2484
- os.tmpdir()
2664
+ os2.tmpdir()
2485
2665
  ];
2486
2666
  for (const orderedPath of orderedPaths) {
2487
2667
  try {
2488
- if (fs6.existsSync(orderedPath)) {
2489
- fs6.accessSync(orderedPath);
2668
+ if (fs7.existsSync(orderedPath)) {
2669
+ fs7.accessSync(orderedPath);
2490
2670
  return orderedPath;
2491
2671
  }
2492
- fs6.mkdirSync(orderedPath, { recursive: true });
2672
+ fs7.mkdirSync(orderedPath, { recursive: true });
2493
2673
  return orderedPath;
2494
2674
  } catch (error) {
2495
2675
  }
@@ -2498,8 +2678,8 @@ var AccessibilityScripts = class _AccessibilityScripts {
2498
2678
  }
2499
2679
  readFromExistingFile() {
2500
2680
  try {
2501
- if (fs6.existsSync(this.commandsPath)) {
2502
- const data = fs6.readFileSync(this.commandsPath, "utf8");
2681
+ if (fs7.existsSync(this.commandsPath)) {
2682
+ const data = fs7.readFileSync(this.commandsPath, "utf8");
2503
2683
  if (data) {
2504
2684
  this.update(JSON.parse(data));
2505
2685
  }
@@ -2522,10 +2702,10 @@ var AccessibilityScripts = class _AccessibilityScripts {
2522
2702
  }
2523
2703
  }
2524
2704
  store() {
2525
- if (!fs6.existsSync(this.browserstackFolderPath)) {
2526
- fs6.mkdirSync(this.browserstackFolderPath);
2705
+ if (!fs7.existsSync(this.browserstackFolderPath)) {
2706
+ fs7.mkdirSync(this.browserstackFolderPath);
2527
2707
  }
2528
- fs6.writeFileSync(this.commandsPath, JSON.stringify({
2708
+ fs7.writeFileSync(this.commandsPath, JSON.stringify({
2529
2709
  commands: this.commandsToWrap,
2530
2710
  scripts: {
2531
2711
  scan: this.performScan,
@@ -2682,7 +2862,10 @@ var launchTestSession = PerformanceTester.measureWrapper(TESTHUB_EVENTS.START, o
2682
2862
  },
2683
2863
  product_map: getProductMapForBuildStartCall(bStackConfig, accessibilityAutomation),
2684
2864
  config: {},
2685
- test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {}
2865
+ test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {},
2866
+ test_management: {
2867
+ test_plan_id: getTestPlanId(options)
2868
+ }
2686
2869
  };
2687
2870
  if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)) {
2688
2871
  data.accessibility.settings ??= {};
@@ -2711,6 +2894,7 @@ var launchTestSession = PerformanceTester.measureWrapper(TESTHUB_EVENTS.START, o
2711
2894
  const jsonResponse = await response.json();
2712
2895
  delete data?.accessibility?.settings?.includeEncodedExtension;
2713
2896
  BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(jsonResponse)}`);
2897
+ BStackLogger.debug(`Test Plan Id sent in request: ${getTestPlanId(options)}`);
2714
2898
  process.env[TESTOPS_BUILD_COMPLETED_ENV] = "true";
2715
2899
  if (jsonResponse.jwt) {
2716
2900
  process.env[BROWSERSTACK_TESTHUB_JWT] = jsonResponse.jwt;
@@ -3220,7 +3404,7 @@ function getObservabilityBuild(options, bstackBuildName) {
3220
3404
  if (options.testObservabilityOptions && options.testObservabilityOptions.buildName) {
3221
3405
  return options.testObservabilityOptions.buildName;
3222
3406
  }
3223
- return bstackBuildName || path6.basename(path6.resolve(process.cwd()));
3407
+ return bstackBuildName || path7.basename(path7.resolve(process.cwd()));
3224
3408
  }
3225
3409
  function getObservabilityBuildTags(options, bstackBuildTag) {
3226
3410
  if (process.env.TEST_OBSERVABILITY_BUILD_TAG) {
@@ -3234,6 +3418,25 @@ function getObservabilityBuildTags(options, bstackBuildTag) {
3234
3418
  }
3235
3419
  return [];
3236
3420
  }
3421
+ function getTestPlanId(options) {
3422
+ if (process.env[BROWSERSTACK_TEST_PLAN_ID]) {
3423
+ return process.env[BROWSERSTACK_TEST_PLAN_ID];
3424
+ }
3425
+ const CLI_ARG = "--browserstack.testManagementOptions.testPlanId";
3426
+ const argIndex = process.argv.indexOf(CLI_ARG);
3427
+ if (argIndex !== -1 && process.argv[argIndex + 1]) {
3428
+ return process.argv[argIndex + 1];
3429
+ }
3430
+ const argWithEquals = process.argv.find((arg) => arg.startsWith(`${CLI_ARG}=`));
3431
+ if (argWithEquals) {
3432
+ return argWithEquals.split("=")[1];
3433
+ }
3434
+ const testPlanId = options.testManagementOptions?.testPlanId;
3435
+ if (typeof testPlanId === "string" && testPlanId.trim().length > 0) {
3436
+ return testPlanId.trim();
3437
+ }
3438
+ return void 0;
3439
+ }
3237
3440
  function getBrowserStackUser(config) {
3238
3441
  if (process.env.BROWSERSTACK_USERNAME) {
3239
3442
  return process.env.BROWSERSTACK_USERNAME;
@@ -3295,6 +3498,45 @@ var getPlatformVersion = o11yErrorHandler(function getPlatformVersion2(caps, use
3295
3498
  }
3296
3499
  return void 0;
3297
3500
  });
3501
+ var getResolvedDeviceName = o11yErrorHandler(function getResolvedDeviceName2(driverCaps, requestedCaps) {
3502
+ const flattenMultiremote = (caps) => {
3503
+ if (!caps) {
3504
+ return [];
3505
+ }
3506
+ const obj = caps;
3507
+ if (obj["deviceModel"] || obj["appium:deviceModel"] || obj["deviceName"] || obj["bstack:options"]) {
3508
+ return [caps];
3509
+ }
3510
+ return Object.values(obj).filter((v) => v !== null && typeof v === "object" && "capabilities" in v).map((v) => v.capabilities).filter(Boolean);
3511
+ };
3512
+ const sources = [
3513
+ ...flattenMultiremote(driverCaps),
3514
+ ...flattenMultiremote(requestedCaps)
3515
+ ];
3516
+ if (!sources.length) {
3517
+ return void 0;
3518
+ }
3519
+ const pickString = (obj, key) => {
3520
+ const v = obj?.[key];
3521
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3522
+ };
3523
+ const paths = [
3524
+ (c) => pickString(c, "deviceModel"),
3525
+ (c) => pickString(c, "appium:deviceModel"),
3526
+ (c) => pickString(c["bstack:options"], "deviceName"),
3527
+ (c) => pickString(c, "appium:deviceName"),
3528
+ (c) => pickString(c, "deviceName")
3529
+ ];
3530
+ for (const path10 of paths) {
3531
+ for (const src of sources) {
3532
+ const v = path10(src);
3533
+ if (v) {
3534
+ return v;
3535
+ }
3536
+ }
3537
+ }
3538
+ return void 0;
3539
+ });
3298
3540
  var isObjectEmpty = (objectName) => {
3299
3541
  return objectName && Object.keys(objectName).length === 0 && objectName.constructor === Object;
3300
3542
  };
@@ -3456,28 +3698,28 @@ function nestedKeyValue(hash, keys) {
3456
3698
  return keys.reduce((hash2, key) => isHash(hash2) ? hash2[key] : void 0, hash);
3457
3699
  }
3458
3700
  function removeDir(dir) {
3459
- const list = fs7.readdirSync(dir);
3701
+ const list = fs8.readdirSync(dir);
3460
3702
  for (let i = 0; i < list.length; i++) {
3461
- const filename = path6.join(dir, list[i]);
3462
- const stat = fs7.statSync(filename);
3703
+ const filename = path7.join(dir, list[i]);
3704
+ const stat = fs8.statSync(filename);
3463
3705
  if (filename === "." || filename === "..") {
3464
3706
  } else if (stat.isDirectory()) {
3465
3707
  removeDir(filename);
3466
3708
  } else {
3467
- fs7.unlinkSync(filename);
3709
+ fs8.unlinkSync(filename);
3468
3710
  }
3469
3711
  }
3470
- fs7.rmdirSync(dir);
3712
+ fs8.rmdirSync(dir);
3471
3713
  }
3472
3714
  function createDir(dir) {
3473
- if (fs7.existsSync(dir)) {
3715
+ if (fs8.existsSync(dir)) {
3474
3716
  removeDir(dir);
3475
3717
  }
3476
- fs7.mkdirSync(dir, { recursive: true });
3718
+ fs8.mkdirSync(dir, { recursive: true });
3477
3719
  }
3478
3720
  function isWritable(dirPath) {
3479
3721
  try {
3480
- fs7.accessSync(dirPath, fs7.constants.W_OK);
3722
+ fs8.accessSync(dirPath, fs8.constants.W_OK);
3481
3723
  return true;
3482
3724
  } catch {
3483
3725
  return false;
@@ -3485,7 +3727,7 @@ function isWritable(dirPath) {
3485
3727
  }
3486
3728
  function setReadWriteAccess(dirPath) {
3487
3729
  try {
3488
- fs7.chmodSync(dirPath, 438);
3730
+ fs8.chmodSync(dirPath, 438);
3489
3731
  BStackLogger.debug(`Directory ${dirPath} is now read/write accessible.`);
3490
3732
  } catch (err) {
3491
3733
  BStackLogger.error(`Failed to set directory access: ${err.stack}`);
@@ -3493,19 +3735,19 @@ function setReadWriteAccess(dirPath) {
3493
3735
  }
3494
3736
 
3495
3737
  // src/cleanup.ts
3496
- import fs10 from "node:fs";
3738
+ import fs11 from "node:fs";
3497
3739
  import util5 from "node:util";
3498
3740
 
3499
3741
  // src/instrumentation/funnelInstrumentation.ts
3500
- import os2 from "node:os";
3742
+ import os3 from "node:os";
3501
3743
  import util4, { format as format2 } from "node:util";
3502
- import path8 from "node:path";
3503
- import fs9 from "node:fs";
3744
+ import path9 from "node:path";
3745
+ import fs10 from "node:fs";
3504
3746
 
3505
3747
  // src/data-store.ts
3506
- import path7 from "node:path";
3507
- import fs8 from "node:fs";
3508
- var workersDataDirPath = path7.join(process.cwd(), "logs", "worker_data");
3748
+ import path8 from "node:path";
3749
+ import fs9 from "node:fs";
3750
+ var workersDataDirPath = path8.join(process.cwd(), "logs", "worker_data");
3509
3751
 
3510
3752
  // src/instrumentation/funnelInstrumentation.ts
3511
3753
  function redactCredentialsFromFunnelData(data) {
@@ -3611,7 +3853,7 @@ Visit https://automation.browserstack.com/builds/${process.env[BROWSERSTACK_TEST
3611
3853
  if (!filePath) {
3612
3854
  return null;
3613
3855
  }
3614
- const content = fs10.readFileSync(filePath, "utf8");
3856
+ const content = fs11.readFileSync(filePath, "utf8");
3615
3857
  const data = JSON.parse(content);
3616
3858
  this.removeFunnelDataFile(filePath);
3617
3859
  return data;
@@ -3620,7 +3862,7 @@ Visit https://automation.browserstack.com/builds/${process.env[BROWSERSTACK_TEST
3620
3862
  if (!filePath) {
3621
3863
  return;
3622
3864
  }
3623
- fs10.rmSync(filePath, { force: true });
3865
+ fs11.rmSync(filePath, { force: true });
3624
3866
  }
3625
3867
  };
3626
3868
  void BStackCleanup.startCleanup();