@wdio/browserstack-service 9.27.1 → 9.27.2

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/index.js CHANGED
@@ -23,7 +23,7 @@ var init_package = __esm({
23
23
  "package.json"() {
24
24
  package_default = {
25
25
  name: "@wdio/browserstack-service",
26
- version: "9.27.0",
26
+ version: "9.27.1",
27
27
  description: "WebdriverIO service for better Browserstack integration",
28
28
  author: "Adam Bjerstedt <abjerstedt@gmail.com>",
29
29
  homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-browserstack-service",
@@ -70,7 +70,6 @@ var init_package = __esm({
70
70
  "browserstack-local": "^1.5.1",
71
71
  chalk: "^5.3.0",
72
72
  "csv-writer": "^1.6.0",
73
- "formdata-node": "5.0.1",
74
73
  "git-repo-info": "^2.1.1",
75
74
  gitconfiglocal: "^2.1.0",
76
75
  glob: "^11.0.0",
@@ -98,7 +97,7 @@ var init_package = __esm({
98
97
  });
99
98
 
100
99
  // src/constants.ts
101
- var bstackServiceVersion, BROWSER_DESCRIPTION, VALID_APP_EXTENSION, DEFAULT_OPTIONS, consoleHolder, APP_ALLY_ISSUES_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, DATA_EVENT_ENDPOINT, DATA_BATCH_ENDPOINT, DATA_SCREENSHOT_ENDPOINT, DATA_BATCH_SIZE, DATA_BATCH_INTERVAL, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS, BSTACK_SERVICE_VERSION, NOT_ALLOWED_KEYS_IN_CAPS, LOGS_FILE, CLI_DEBUG_LOGS_FILE, UPLOAD_LOGS_ENDPOINT, PERCY_LOGS_FILE, PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES, LOG_KIND_USAGE_MAP, SUPPORTED_BROWSERS_FOR_AI, TCG_URL, TCG_INFO, SMART_SELECTION_MODE_RELEVANT_FIRST, SMART_SELECTION_MODE_RELEVANT_ONLY, BROWSERSTACK_TESTHUB_JWT, BSTACK_TCG_AUTH_RESULT, TESTOPS_SCREENSHOT_ENV, BROWSERSTACK_TESTHUB_UUID, TEST_ANALYTICS_ID, PERF_MEASUREMENT_ENV, RERUN_TESTS_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_PERCY, BROWSERSTACK_ACCESSIBILITY, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_TEST_REPORTING, TEST_REPORTING_PROJECT_NAME, MAX_GIT_META_DATA_SIZE_IN_BYTES, GIT_META_DATA_TRUNCATED, CLI_STOP_TIMEOUT, WDIO_NAMING_PREFIX, UPDATED_CLI_ENDPOINT;
100
+ var bstackServiceVersion, BROWSER_DESCRIPTION, VALID_APP_EXTENSION, DEFAULT_OPTIONS, consoleHolder, APP_ALLY_ISSUES_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, DATA_EVENT_ENDPOINT, DATA_BATCH_ENDPOINT, DATA_SCREENSHOT_ENDPOINT, DATA_BATCH_SIZE, DATA_BATCH_INTERVAL, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS, BSTACK_SERVICE_VERSION, NOT_ALLOWED_KEYS_IN_CAPS, BROWSERSTACK_TEST_PLAN_ID, LOGS_FILE, CLI_DEBUG_LOGS_FILE, UPLOAD_LOGS_ENDPOINT, PERCY_LOGS_FILE, PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES, LOG_KIND_USAGE_MAP, SUPPORTED_BROWSERS_FOR_AI, TCG_URL, TCG_INFO, SMART_SELECTION_MODE_RELEVANT_FIRST, SMART_SELECTION_MODE_RELEVANT_ONLY, BROWSERSTACK_TESTHUB_JWT, BSTACK_TCG_AUTH_RESULT, TESTOPS_SCREENSHOT_ENV, BROWSERSTACK_TESTHUB_UUID, TEST_ANALYTICS_ID, PERF_MEASUREMENT_ENV, RERUN_TESTS_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_PERCY, BROWSERSTACK_ACCESSIBILITY, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_TEST_REPORTING, TEST_REPORTING_PROJECT_NAME, MAX_GIT_META_DATA_SIZE_IN_BYTES, GIT_META_DATA_TRUNCATED, CLI_STOP_TIMEOUT, BINARY_BUSY_ERROR_CODES, MAX_SPAWN_RETRIES, SPAWN_RETRY_DELAY_MS, WDIO_NAMING_PREFIX, UPDATED_CLI_ENDPOINT;
102
101
  var init_constants = __esm({
103
102
  "src/constants.ts"() {
104
103
  "use strict";
@@ -122,8 +121,7 @@ var init_constants = __esm({
122
121
  DEFAULT_OPTIONS = {
123
122
  setSessionName: true,
124
123
  setSessionStatus: true,
125
- testObservability: true,
126
- accessibility: false
124
+ testObservability: true
127
125
  };
128
126
  consoleHolder = Object.assign({}, console);
129
127
  APP_ALLY_ISSUES_ENDPOINT = "api/v1/issues";
@@ -136,7 +134,8 @@ var init_constants = __esm({
136
134
  DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5e3;
137
135
  DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100;
138
136
  BSTACK_SERVICE_VERSION = bstackServiceVersion;
139
- NOT_ALLOWED_KEYS_IN_CAPS = ["includeTagsInTestingScope", "excludeTagsInTestingScope"];
137
+ NOT_ALLOWED_KEYS_IN_CAPS = ["includeTagsInTestingScope", "excludeTagsInTestingScope", "testManagementOptions"];
138
+ BROWSERSTACK_TEST_PLAN_ID = "BROWSERSTACK_TEST_PLAN_ID";
140
139
  LOGS_FILE = "logs/bstack-wdio-service.log";
141
140
  CLI_DEBUG_LOGS_FILE = "log/sdk-cli-debug.log";
142
141
  UPLOAD_LOGS_ENDPOINT = "client-logs/upload";
@@ -182,6 +181,9 @@ var init_constants = __esm({
182
181
  MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024;
183
182
  GIT_META_DATA_TRUNCATED = "...[TRUNCATED]";
184
183
  CLI_STOP_TIMEOUT = 5e3;
184
+ BINARY_BUSY_ERROR_CODES = ["ETXTBSY", "EBUSY"];
185
+ MAX_SPAWN_RETRIES = 3;
186
+ SPAWN_RETRY_DELAY_MS = 1e3;
185
187
  WDIO_NAMING_PREFIX = "WebdriverIO-";
186
188
  UPDATED_CLI_ENDPOINT = "sdk/v1/update_cli";
187
189
  }
@@ -635,6 +637,7 @@ var init_cliUtils = __esm({
635
637
  modifiedOpts.browserStackLocalOptions = modifiedOpts.opts;
636
638
  delete modifiedOpts.opts;
637
639
  }
640
+ delete modifiedOpts.testManagementOptions;
638
641
  modifiedOpts.testContextOptions = {
639
642
  skipSessionName: isFalse(modifiedOpts.setSessionName),
640
643
  skipSessionStatus: isFalse(modifiedOpts.setSessionStatus),
@@ -653,6 +656,8 @@ var init_cliUtils = __esm({
653
656
  options.testObservability
654
657
  );
655
658
  const observabilityOptions = options.testObservabilityOptions || {};
659
+ const testManagementOptions = options.testManagementOptions || {};
660
+ const testPlanId = typeof testManagementOptions.testPlanId === "string" ? testManagementOptions.testPlanId.trim() : "";
656
661
  const binconfig = {
657
662
  userName: observabilityOptions.user || config.user,
658
663
  accessKey: observabilityOptions.key || config.key,
@@ -664,26 +669,26 @@ var init_cliUtils = __esm({
664
669
  binconfig.buildName = observabilityOptions.buildName || binconfig.buildName;
665
670
  binconfig.projectName = observabilityOptions.projectName || binconfig.projectName;
666
671
  binconfig.buildTag = this.getObservabilityBuildTags(observabilityOptions, buildTag) || [];
667
- let caps = capabilities;
668
- if (capabilities && !Array.isArray(capabilities)) {
669
- caps = [capabilities];
670
- }
671
- if (Array.isArray(caps)) {
672
- for (const cap of caps) {
673
- const platform4 = {};
674
- const capability = cap;
675
- Object.keys(capability).filter((key) => key !== "bstack:options").forEach((key) => {
676
- platform4[key] = capability[key];
672
+ if (testPlanId.length > 0) {
673
+ binconfig.testManagementOptions = {
674
+ testPlanId
675
+ };
676
+ }
677
+ const caps = Array.isArray(capabilities) ? capabilities : [capabilities];
678
+ for (const cap of caps) {
679
+ const platform4 = {};
680
+ const capability = cap;
681
+ Object.keys(capability).filter((key) => key !== "bstack:options").forEach((key) => {
682
+ platform4[key] = capability[key];
683
+ });
684
+ if (capability["bstack:options"]) {
685
+ Object.keys(
686
+ capability["bstack:options"]
687
+ ).forEach((key) => {
688
+ platform4[key] = capability["bstack:options"][key];
677
689
  });
678
- if (capability["bstack:options"]) {
679
- Object.keys(
680
- capability["bstack:options"]
681
- ).forEach((key) => {
682
- platform4[key] = capability["bstack:options"][key];
683
- });
684
- }
685
- binconfig.platforms.push(platform4);
686
690
  }
691
+ binconfig.platforms.push(platform4);
687
692
  }
688
693
  return JSON.stringify(binconfig);
689
694
  }
@@ -742,9 +747,20 @@ var init_cliUtils = __esm({
742
747
  sdk_language: this.getSdkLanguage()
743
748
  };
744
749
  if (!isNullOrEmpty(existingCliPath)) {
745
- queryParams.cli_version = await this.runShellCommand(
750
+ if (this.isBinaryBusy(existingCliPath)) {
751
+ BStackLogger.warn(`Existing binary is currently in use, skipping update: ${existingCliPath}`);
752
+ PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE);
753
+ return existingCliPath;
754
+ }
755
+ const version3 = await this.runShellCommand(
746
756
  `${existingCliPath} version`
747
757
  );
758
+ if (version3.toLowerCase().includes("text file busy")) {
759
+ BStackLogger.warn(`Binary busy during version check, skipping update: ${existingCliPath}`);
760
+ PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE);
761
+ return existingCliPath;
762
+ }
763
+ queryParams.cli_version = version3;
748
764
  }
749
765
  const response = await this.requestToUpdateCLI(queryParams, config);
750
766
  if (nestedKeyValue(response, ["updated_cli_version"])) {
@@ -860,6 +876,29 @@ var init_cliUtils = __esm({
860
876
  return "";
861
877
  }
862
878
  }
879
+ static isBinaryBusy(binaryPath) {
880
+ if (isNullOrEmpty(binaryPath)) {
881
+ return false;
882
+ }
883
+ if (platform() === "darwin") {
884
+ return false;
885
+ }
886
+ if (!fs2.existsSync(binaryPath)) {
887
+ return false;
888
+ }
889
+ try {
890
+ const fd = fs2.openSync(binaryPath, "r+");
891
+ fs2.closeSync(fd);
892
+ return false;
893
+ } catch (err) {
894
+ if (BINARY_BUSY_ERROR_CODES.includes(err.code)) {
895
+ BStackLogger.debug(`Binary is busy: ${binaryPath}`);
896
+ return true;
897
+ }
898
+ BStackLogger.debug(`Error checking if binary is busy: ${err.message}`);
899
+ return false;
900
+ }
901
+ }
863
902
  static requestToUpdateCLI = async (queryParams, config) => {
864
903
  const params = new URLSearchParams(queryParams);
865
904
  const requestInit = {
@@ -980,8 +1019,11 @@ ${Date.now()}
980
1019
  const cleanupTemporaryDownloads = (maxAgeMs = CLI_LOCK_TIMEOUT_MS) => {
981
1020
  try {
982
1021
  const now2 = Date.now();
1022
+ const tmpExtractRe = /\.tmp\.\d+$/;
983
1023
  for (const entry of fs2.readdirSync(cliDir)) {
984
- if (!entry.startsWith(CLI_DOWNLOAD_TMP_PREFIX) || !entry.endsWith(CLI_DOWNLOAD_TMP_SUFFIX)) {
1024
+ const isDownloadZip = entry.startsWith(CLI_DOWNLOAD_TMP_PREFIX) && entry.endsWith(CLI_DOWNLOAD_TMP_SUFFIX);
1025
+ const isExtractTmp = tmpExtractRe.test(entry);
1026
+ if (!isDownloadZip && !isExtractTmp) {
985
1027
  continue;
986
1028
  }
987
1029
  const filePath = path2.join(cliDir, entry);
@@ -998,13 +1040,13 @@ ${Date.now()}
998
1040
  fs2.unlinkSync(filePath);
999
1041
  } catch (err) {
1000
1042
  BStackLogger.debug(
1001
- `Failed to delete temp CLI zip file ${filePath}: ${util.format(err)}`
1043
+ `Failed to delete temp CLI file ${filePath}: ${util.format(err)}`
1002
1044
  );
1003
1045
  }
1004
1046
  }
1005
1047
  } catch (err) {
1006
1048
  BStackLogger.debug(
1007
- `Failed to scan temp CLI downloads in ${cliDir}: ${util.format(err)}`
1049
+ `Failed to scan temp CLI files in ${cliDir}: ${util.format(err)}`
1008
1050
  );
1009
1051
  }
1010
1052
  };
@@ -1050,7 +1092,6 @@ ${Date.now()}
1050
1092
  );
1051
1093
  const downloadedFileStream = fs2.createWriteStream(zipFilePath);
1052
1094
  return new Promise((resolve, reject) => {
1053
- const binaryName = null;
1054
1095
  const processDownload = async () => {
1055
1096
  const abortController = new AbortController();
1056
1097
  const timeout = setTimeout(
@@ -1084,7 +1125,6 @@ ${Date.now()}
1084
1125
  nodeStream.pipe(downloadedFileStream);
1085
1126
  _CLIUtils.downloadFileStream(
1086
1127
  downloadedFileStream,
1087
- binaryName,
1088
1128
  zipFilePath,
1089
1129
  cliDir,
1090
1130
  (result) => {
@@ -1118,40 +1158,104 @@ ${Date.now()}
1118
1158
  return null;
1119
1159
  }
1120
1160
  };
1121
- static downloadFileStream(downloadedFileStream, binaryName, zipFilePath, cliDir, resolve, reject) {
1161
+ static downloadFileStream(downloadedFileStream, zipFilePath, cliDir, resolve, reject) {
1122
1162
  downloadedFileStream.on("close", async function() {
1123
1163
  const yauzlOpenPromise = promisify(yauzl.open);
1124
1164
  try {
1125
1165
  const zipfile = await yauzlOpenPromise(zipFilePath, {
1126
1166
  lazyEntries: true
1127
1167
  });
1168
+ let resolvedBinaryPath = null;
1128
1169
  zipfile.readEntry();
1129
1170
  zipfile.on("entry", async (entry) => {
1130
- if (!binaryName) {
1131
- binaryName = entry.fileName;
1132
- }
1133
1171
  if (/\/$/.test(entry.fileName)) {
1134
1172
  zipfile.readEntry();
1135
- } else {
1136
- const writeStream = fs2.createWriteStream(
1137
- path2.join(cliDir, entry.fileName)
1138
- );
1139
- const openReadStreamPromise = promisify(
1173
+ return;
1174
+ }
1175
+ const candidatePath = path2.join(cliDir, entry.fileName);
1176
+ const resolvedCandidate = path2.resolve(candidatePath);
1177
+ const resolvedDir = path2.resolve(cliDir) + path2.sep;
1178
+ if (!resolvedCandidate.startsWith(resolvedDir)) {
1179
+ zipfile.close();
1180
+ reject(new Error(`Zip-slip detected: entry "${entry.fileName}" resolves outside ${cliDir}`));
1181
+ return;
1182
+ }
1183
+ const isBinaryEntry = path2.basename(entry.fileName).startsWith("binary-");
1184
+ if (!isBinaryEntry) {
1185
+ const directStream = fs2.createWriteStream(candidatePath);
1186
+ directStream.on("error", (writeErr) => {
1187
+ zipfile.close();
1188
+ reject(writeErr);
1189
+ });
1190
+ const openReadStreamPromise2 = promisify(
1140
1191
  zipfile.openReadStream
1141
1192
  ).bind(zipfile);
1142
1193
  try {
1143
- const readStream = await openReadStreamPromise(entry);
1194
+ const readStream = await openReadStreamPromise2(entry);
1144
1195
  readStream.on("end", function() {
1145
- writeStream.close();
1146
- zipfile.readEntry();
1196
+ directStream.end();
1197
+ directStream.on("close", () => zipfile.readEntry());
1147
1198
  });
1148
- readStream.pipe(writeStream);
1199
+ readStream.pipe(directStream);
1149
1200
  } catch (zipErr) {
1201
+ zipfile.close();
1150
1202
  reject(zipErr);
1151
1203
  }
1152
- if (entry.fileName === binaryName) {
1204
+ return;
1205
+ }
1206
+ const finalPath = candidatePath;
1207
+ const tempPath = path2.join(cliDir, `${entry.fileName}.tmp.${process.pid}`);
1208
+ const writeStream = fs2.createWriteStream(tempPath);
1209
+ let writeStreamErrored = false;
1210
+ writeStream.on("error", (writeErr) => {
1211
+ writeStreamErrored = true;
1212
+ fsp.unlink(tempPath).catch(() => {
1213
+ });
1214
+ zipfile.close();
1215
+ reject(writeErr);
1216
+ });
1217
+ writeStream.on("close", async () => {
1218
+ if (writeStreamErrored) {
1219
+ return;
1220
+ }
1221
+ try {
1222
+ await fsp.chmod(tempPath, "0755");
1223
+ try {
1224
+ await fsp.rename(tempPath, finalPath);
1225
+ } catch (renameErr) {
1226
+ if (renameErr.code !== "EXDEV") {
1227
+ throw renameErr;
1228
+ }
1229
+ BStackLogger.warn(`Atomic rename failed (cross-device), falling back to copy: ${renameErr.message}`);
1230
+ await fsp.copyFile(tempPath, finalPath);
1231
+ await fsp.unlink(tempPath).catch(() => {
1232
+ });
1233
+ }
1234
+ if (!resolvedBinaryPath) {
1235
+ resolvedBinaryPath = finalPath;
1236
+ }
1237
+ zipfile.readEntry();
1238
+ } catch (err) {
1239
+ await fsp.unlink(tempPath).catch(() => {
1240
+ });
1153
1241
  zipfile.close();
1242
+ reject(err);
1154
1243
  }
1244
+ });
1245
+ const openReadStreamPromise = promisify(
1246
+ zipfile.openReadStream
1247
+ ).bind(zipfile);
1248
+ try {
1249
+ const readStream = await openReadStreamPromise(entry);
1250
+ readStream.on("end", function() {
1251
+ writeStream.end();
1252
+ });
1253
+ readStream.pipe(writeStream);
1254
+ } catch (zipErr) {
1255
+ fsp.unlink(tempPath).catch(() => {
1256
+ });
1257
+ zipfile.close();
1258
+ reject(zipErr);
1155
1259
  }
1156
1260
  });
1157
1261
  zipfile.on("error", (zipErr) => {
@@ -1159,16 +1263,15 @@ ${Date.now()}
1159
1263
  });
1160
1264
  zipfile.once("end", () => {
1161
1265
  fsp.unlink(zipFilePath).catch(() => {
1162
- BStackLogger.warn(
1163
- `Failed to delete zip file: ${zipFilePath}`
1164
- );
1165
- });
1166
- fsp.chmod(`${cliDir}/${binaryName}`, "0755").then(() => {
1167
- resolve(`${cliDir}/${binaryName}`);
1168
- }).catch((err) => {
1169
- reject(err);
1266
+ BStackLogger.warn(`Failed to delete zip file: ${zipFilePath}`);
1170
1267
  });
1268
+ if (!resolvedBinaryPath) {
1269
+ zipfile.close();
1270
+ reject(new Error("No binary-* entry found in zip; cannot complete CLI binary extraction"));
1271
+ return;
1272
+ }
1171
1273
  zipfile.close();
1274
+ resolve(resolvedBinaryPath);
1172
1275
  });
1173
1276
  } catch (err) {
1174
1277
  reject(err);
@@ -1525,6 +1628,7 @@ var init_performance_tester = __esm({
1525
1628
  static end(event, success = true, failure, details = {}) {
1526
1629
  performance.mark(event + "-end");
1527
1630
  performance.measure(event, event + "-start", event + "-end");
1631
+ delete this.eventsMap[event + "-start"];
1528
1632
  this.details[event] = Object.assign({ success, failure: util2.format(failure) }, Object.assign(Object.assign({
1529
1633
  clientWorkerId: _PerformanceTester.getClientWorkerId(),
1530
1634
  worker: _PerformanceTester.getProcessId(),
@@ -2617,10 +2721,8 @@ import path5 from "node:path";
2617
2721
  import util3 from "node:util";
2618
2722
  import gitRepoInfo from "git-repo-info";
2619
2723
  import gitconfig from "gitconfiglocal";
2620
- import { FormData as FormData2 } from "formdata-node";
2621
2724
  import { performance as performance2 } from "node:perf_hooks";
2622
2725
  import { create } from "tar";
2623
- import { fileFromPath } from "formdata-node/file-from-path";
2624
2726
  function getBrowserDescription(cap) {
2625
2727
  cap = cap || {};
2626
2728
  if (cap["bstack:options"]) {
@@ -3190,6 +3292,25 @@ function getObservabilityBuildTags(options, bstackBuildTag) {
3190
3292
  }
3191
3293
  return [];
3192
3294
  }
3295
+ function getTestPlanId(options) {
3296
+ if (process.env[BROWSERSTACK_TEST_PLAN_ID]) {
3297
+ return process.env[BROWSERSTACK_TEST_PLAN_ID];
3298
+ }
3299
+ const CLI_ARG = "--browserstack.testManagementOptions.testPlanId";
3300
+ const argIndex = process.argv.indexOf(CLI_ARG);
3301
+ if (argIndex !== -1 && process.argv[argIndex + 1]) {
3302
+ return process.argv[argIndex + 1];
3303
+ }
3304
+ const argWithEquals = process.argv.find((arg) => arg.startsWith(`${CLI_ARG}=`));
3305
+ if (argWithEquals) {
3306
+ return argWithEquals.split("=")[1];
3307
+ }
3308
+ const testPlanId = options.testManagementOptions?.testPlanId;
3309
+ if (typeof testPlanId === "string" && testPlanId.trim().length > 0) {
3310
+ return testPlanId.trim();
3311
+ }
3312
+ return void 0;
3313
+ }
3193
3314
  function getBrowserStackUser(config) {
3194
3315
  if (process.env.BROWSERSTACK_USERNAME) {
3195
3316
  return process.env.BROWSERSTACK_USERNAME;
@@ -3235,8 +3356,14 @@ function getFailureObject(error) {
3235
3356
  };
3236
3357
  }
3237
3358
  async function uploadLogs(user, key, clientBuildUuid) {
3359
+ const eventName = EVENTS.SDK_UPLOAD_LOGS;
3360
+ let success = true;
3361
+ let failure;
3362
+ PerformanceTester.start(eventName);
3238
3363
  try {
3239
3364
  if (!user || !key) {
3365
+ success = false;
3366
+ failure = "skipped: missing_credentials";
3240
3367
  BStackLogger2.debug("Uploading logs failed due to no credentials");
3241
3368
  return;
3242
3369
  }
@@ -3248,10 +3375,20 @@ async function uploadLogs(user, key, clientBuildUuid) {
3248
3375
  CLI_DEBUG_LOGS_FILE
3249
3376
  ].filter((f) => fs6.existsSync(f));
3250
3377
  const copiedFileNames = [];
3378
+ const archiveAddFailures = [];
3251
3379
  for (const f of filesToArchive) {
3252
- const dest = path5.join(tmpDir, path5.basename(f));
3253
- fs6.copyFileSync(f, dest);
3254
- copiedFileNames.push(path5.basename(f));
3380
+ try {
3381
+ const dest = path5.join(tmpDir, path5.basename(f));
3382
+ fs6.copyFileSync(f, dest);
3383
+ copiedFileNames.push(path5.basename(f));
3384
+ } catch (copyErr) {
3385
+ const msg = copyErr?.message || String(copyErr);
3386
+ archiveAddFailures.push(`${path5.basename(f)}: ${msg}`);
3387
+ }
3388
+ }
3389
+ if (archiveAddFailures.length > 0 && failure === void 0) {
3390
+ success = false;
3391
+ failure = `archive_add_failed [${archiveAddFailures.length}]: ${archiveAddFailures.join("; ")}`.substring(0, 300);
3255
3392
  }
3256
3393
  await create(
3257
3394
  {
@@ -3270,8 +3407,8 @@ async function uploadLogs(user, key, clientBuildUuid) {
3270
3407
  dest.on("finish", resolve);
3271
3408
  dest.on("error", reject);
3272
3409
  });
3273
- const formData = new FormData2();
3274
- const file = await fileFromPath(tarGzPath);
3410
+ const formData = new FormData();
3411
+ const file = await fs6.openAsBlob(tarGzPath, { type: "application/x-gzip" });
3275
3412
  formData.append("data", file, "logs.tar.gz");
3276
3413
  formData.append("clientBuildUuid", clientBuildUuid);
3277
3414
  const auth = Buffer.from(`${user}:${key}`).toString("base64");
@@ -3298,10 +3435,21 @@ async function uploadLogs(user, key, clientBuildUuid) {
3298
3435
  if (fs6.existsSync(CLI_DEBUG_LOGS_FILE)) {
3299
3436
  fs6.unlinkSync(CLI_DEBUG_LOGS_FILE);
3300
3437
  }
3438
+ if (!response) {
3439
+ success = false;
3440
+ failure = "upload_no_response";
3441
+ } else if (response.status && response.status !== "success") {
3442
+ success = false;
3443
+ failure = `upload_status: ${response.status}${response.message ? " - " + String(response.message).slice(0, 200) : ""}`;
3444
+ }
3301
3445
  return response;
3302
3446
  } catch (error) {
3447
+ success = false;
3448
+ failure = `uploadLogs exception: ${getErrorString(error)}`;
3303
3449
  BStackLogger2.error(`Error while uploading logs: ${getErrorString(error)}`);
3304
3450
  return null;
3451
+ } finally {
3452
+ PerformanceTester.end(eventName, success, failure);
3305
3453
  }
3306
3454
  }
3307
3455
  function truncateString(field, truncateSizeInBytes) {
@@ -3698,7 +3846,10 @@ var init_util = __esm({
3698
3846
  },
3699
3847
  product_map: getProductMapForBuildStartCall(bStackConfig, accessibilityAutomation),
3700
3848
  config: {},
3701
- test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {}
3849
+ test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {},
3850
+ test_management: {
3851
+ test_plan_id: getTestPlanId(options)
3852
+ }
3702
3853
  };
3703
3854
  if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)) {
3704
3855
  data.accessibility.settings ??= {};
@@ -3727,6 +3878,7 @@ var init_util = __esm({
3727
3878
  const jsonResponse = await response.json();
3728
3879
  delete data?.accessibility?.settings?.includeEncodedExtension;
3729
3880
  BStackLogger2.debug(`[Start_Build] Success response: ${JSON.stringify(jsonResponse)}`);
3881
+ BStackLogger2.debug(`Test Plan Id sent in request: ${getTestPlanId(options)}`);
3730
3882
  process.env[TESTOPS_BUILD_COMPLETED_ENV] = "true";
3731
3883
  if (jsonResponse.jwt) {
3732
3884
  process.env[BROWSERSTACK_TESTHUB_JWT] = jsonResponse.jwt;
@@ -4201,13 +4353,14 @@ var init_utils = __esm({
4201
4353
  init_bstackLogger();
4202
4354
  init_util();
4203
4355
  getProductMap = (config) => {
4204
- return {
4205
- observability: config.testObservability.enabled,
4206
- accessibility: config.accessibility,
4207
- percy: config.percy,
4208
- automate: config.automate,
4209
- app_automate: config.appAutomate
4210
- };
4356
+ const entries = [
4357
+ ["observability", config.testObservability.enabled],
4358
+ ["accessibility", config.accessibility],
4359
+ ["percy", config.percy],
4360
+ ["automate", config.automate],
4361
+ ["app_automate", config.appAutomate]
4362
+ ];
4363
+ return Object.fromEntries(entries.filter(([, v]) => v !== null));
4211
4364
  };
4212
4365
  shouldProcessEventForTesthub = (eventType) => {
4213
4366
  if (isTrue(process.env[BROWSERSTACK_OBSERVABILITY])) {
@@ -4255,13 +4408,14 @@ var init_utils = __esm({
4255
4408
  }
4256
4409
  };
4257
4410
  getProductMapForBuildStartCall = (config, accessibilityAutomation) => {
4258
- return {
4259
- observability: config.testObservability.enabled,
4260
- accessibility: accessibilityAutomation,
4261
- percy: config.percy,
4262
- automate: config.automate,
4263
- app_automate: config.appAutomate
4264
- };
4411
+ const entries = [
4412
+ ["observability", config.testObservability.enabled],
4413
+ ["accessibility", accessibilityAutomation],
4414
+ ["percy", config.percy],
4415
+ ["automate", config.automate],
4416
+ ["app_automate", config.appAutomate]
4417
+ ];
4418
+ return Object.fromEntries(entries.filter(([, v]) => v !== null));
4265
4419
  };
4266
4420
  }
4267
4421
  });
@@ -4359,7 +4513,7 @@ import {
4359
4513
  DriverInitRequestConstructor,
4360
4514
  FetchDriverExecuteParamsEventRequestConstructor
4361
4515
  } from "@browserstack/wdio-browserstack-service";
4362
- var GrpcClient;
4516
+ var GRPC_MESSAGE_LIMIT, GrpcClient;
4363
4517
  var init_grpcClient = __esm({
4364
4518
  "src/cli/grpcClient.ts"() {
4365
4519
  "use strict";
@@ -4367,6 +4521,7 @@ var init_grpcClient = __esm({
4367
4521
  init_performance_tester();
4368
4522
  init_constants2();
4369
4523
  init_cliLogger();
4524
+ GRPC_MESSAGE_LIMIT = 20 * 1024 * 1024;
4370
4525
  GrpcClient = class _GrpcClient {
4371
4526
  static #instance = null;
4372
4527
  binSessionId;
@@ -4428,17 +4583,20 @@ var init_grpcClient = __esm({
4428
4583
  if (!listenAddress) {
4429
4584
  throw new Error("Unable to determine gRPC server listen address");
4430
4585
  }
4586
+ const channelOptions = {
4587
+ "grpc.keepalive_time_ms": 1e4,
4588
+ "grpc.max_send_message_length": GRPC_MESSAGE_LIMIT,
4589
+ "grpc.max_receive_message_length": GRPC_MESSAGE_LIMIT
4590
+ };
4431
4591
  this.channel = new grpcChannel(
4432
4592
  listenAddress,
4433
4593
  grpcCredentials.createInsecure(),
4434
- {
4435
- "grpc.keepalive_time_ms": 1e4
4436
- }
4594
+ channelOptions
4437
4595
  );
4438
4596
  this.client = new SDKClient(
4439
4597
  listenAddress,
4440
4598
  grpcCredentials.createInsecure(),
4441
- {}
4599
+ channelOptions
4442
4600
  );
4443
4601
  this.logger.info(`Connected to gRPC server at ${listenAddress}`);
4444
4602
  }
@@ -5678,12 +5836,12 @@ var init_automateModule = __esm({
5678
5836
  async onAfterTest(args) {
5679
5837
  this.logger.debug("onAfterTest: inside automate module after test hook!");
5680
5838
  const instace = args.instance;
5681
- const { error, passed } = args.result;
5839
+ const { error, passed, skipped } = args.result;
5682
5840
  const _failReasons = [];
5683
- if (!passed) {
5841
+ if (!passed && !skipped) {
5684
5842
  _failReasons.push(error && error.message || "Unknown Error");
5685
5843
  }
5686
- const status = passed ? "passed" : "failed";
5844
+ const status = passed || skipped ? "passed" : "failed";
5687
5845
  const reason = _failReasons.length > 0 ? _failReasons.join("\n") : void 0;
5688
5846
  const autoInstance = AutomationFramework.getTrackedInstance();
5689
5847
  const sessionId = AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID);
@@ -6081,13 +6239,17 @@ var init_wdioMochaTestFramework = __esm({
6081
6239
  }
6082
6240
  loadTestResult(instance, args) {
6083
6241
  const results = args.result;
6084
- const { error, passed } = results;
6242
+ const { error, passed, skipped } = results;
6085
6243
  let result = "passed";
6086
6244
  let failure = null;
6087
6245
  let failureReason = null;
6088
6246
  let failureType = null;
6089
6247
  if (!passed) {
6090
- result = error && error.message && error.message.includes("sync skip; aborting execution") ? "ignore" : "failed";
6248
+ if (skipped) {
6249
+ result = "skipped";
6250
+ } else {
6251
+ result = error && error.message && error.message.includes("sync skip; aborting execution") ? "ignore" : "failed";
6252
+ }
6091
6253
  if (error && result !== "skipped") {
6092
6254
  failure = [{ backtrace: [removeAnsiColors(error.message), removeAnsiColors(error.stack || "")] }];
6093
6255
  failureReason = removeAnsiColors(error.message);
@@ -9179,6 +9341,17 @@ var init_cli = __esm({
9179
9341
  }
9180
9342
  this.modules[ObservabilityModule.MODULE_NAME] = new ObservabilityModule(startBinResponse.observability);
9181
9343
  }
9344
+ if (startBinResponse.testhub.errors && startBinResponse.testhub.errors.length > 0) {
9345
+ try {
9346
+ const errors = JSON.parse(Buffer.from(startBinResponse.testhub.errors).toString());
9347
+ for (const [code, detail] of Object.entries(errors)) {
9348
+ const { message } = detail;
9349
+ BStackLogger.error(`[Build] ${code}: ${message}`);
9350
+ }
9351
+ } catch (e) {
9352
+ BStackLogger.debug(`Failed to parse testhub errors: ${e}`);
9353
+ }
9354
+ }
9182
9355
  this.modules[TestHubModule.MODULE_NAME] = new TestHubModule(startBinResponse.testhub);
9183
9356
  if (startBinResponse.accessibility?.success) {
9184
9357
  process.env[BROWSERSTACK_ACCESSIBILITY] = "true";
@@ -9223,12 +9396,61 @@ var init_cli = __esm({
9223
9396
  return Promise.resolve();
9224
9397
  }
9225
9398
  const SDK_CLI_BIN_PATH = await this.getCliBinPath();
9399
+ if (!SDK_CLI_BIN_PATH) {
9400
+ throw new Error("Failed to get CLI binary path. CLI setup failed.");
9401
+ }
9226
9402
  const cmd = [SDK_CLI_BIN_PATH, "sdk"];
9227
9403
  this.logger.debug(`spawning command='${cmd}'`);
9228
- this.process = spawn3(cmd[0], cmd.slice(1), {
9229
- env: process.env
9230
- });
9404
+ for (let attempt = 1; attempt <= MAX_SPAWN_RETRIES; attempt++) {
9405
+ try {
9406
+ await this._spawnAndAwaitReady(cmd);
9407
+ if (attempt > 1) {
9408
+ this.logger.info(`Spawn succeeded on attempt ${attempt}/${MAX_SPAWN_RETRIES}`);
9409
+ }
9410
+ PerformanceTester.end(EVENTS.SDK_CLI_START);
9411
+ return;
9412
+ } catch (err) {
9413
+ const isBusy = BINARY_BUSY_ERROR_CODES.includes(err.code) || err.message && (err.message.toLowerCase().includes("text file busy") || err.message.toLowerCase().includes("being used by another process"));
9414
+ if (isBusy && attempt < MAX_SPAWN_RETRIES) {
9415
+ this.logger.warn(
9416
+ `Spawn attempt ${attempt}/${MAX_SPAWN_RETRIES} failed with busy error, retrying in ${SPAWN_RETRY_DELAY_MS}ms: ${err.message}`
9417
+ );
9418
+ if (this.process) {
9419
+ this.process.removeAllListeners();
9420
+ this.process.stdout?.removeAllListeners();
9421
+ this.process.stderr?.removeAllListeners();
9422
+ this.process = null;
9423
+ }
9424
+ await new Promise((r) => setTimeout(r, SPAWN_RETRY_DELAY_MS));
9425
+ } else {
9426
+ PerformanceTester.end(EVENTS.SDK_CLI_START, false, err);
9427
+ throw err;
9428
+ }
9429
+ }
9430
+ }
9431
+ }
9432
+ /**
9433
+ * Spawn the CLI process and wait until it emits "ready".
9434
+ * Handles both synchronous spawn errors and async error/close events.
9435
+ */
9436
+ _spawnAndAwaitReady(cmd) {
9231
9437
  return new Promise((resolve, reject) => {
9438
+ let settled = false;
9439
+ const settle = (fn, val) => {
9440
+ if (settled) {
9441
+ return;
9442
+ }
9443
+ settled = true;
9444
+ fn(val);
9445
+ };
9446
+ try {
9447
+ this.process = spawn3(cmd[0], cmd.slice(1), {
9448
+ env: process.env
9449
+ });
9450
+ } catch (syncErr) {
9451
+ settle(reject, syncErr);
9452
+ return;
9453
+ }
9232
9454
  const cliOut = {};
9233
9455
  this.process.stdout.on("data", (data) => {
9234
9456
  const lines = data.toString().trim().split("\n");
@@ -9241,8 +9463,7 @@ var init_cli = __esm({
9241
9463
  }
9242
9464
  if (line.toLowerCase().includes("ready")) {
9243
9465
  this.loadCliParams(cliOut);
9244
- PerformanceTester.end(EVENTS.SDK_CLI_START);
9245
- resolve();
9466
+ settle(resolve);
9246
9467
  return;
9247
9468
  }
9248
9469
  }
@@ -9251,14 +9472,11 @@ var init_cli = __esm({
9251
9472
  this.logger.error(`CLI stderr: ${data.toString().trim()}`);
9252
9473
  });
9253
9474
  this.process.on("error", (err) => {
9254
- cliOut.error = `Error in start: ${err.message}`;
9255
- PerformanceTester.end(EVENTS.SDK_CLI_START, false, err);
9256
- reject(new Error(`Failed to start CLI process: ${err.message}`));
9475
+ settle(reject, err);
9257
9476
  });
9258
9477
  this.process.on("close", (code) => {
9259
- if (code !== 0) {
9260
- reject(new Error(`CLI process exited with code ${code}`));
9261
- }
9478
+ const msg = code !== 0 ? `CLI process exited with code ${code}` : "CLI process exited cleanly before emitting ready";
9479
+ settle(reject, new Error(msg));
9262
9480
  });
9263
9481
  });
9264
9482
  }
@@ -10669,7 +10887,7 @@ var BrowserStackConfig = class _BrowserStackConfig {
10669
10887
  this.accessKey = config.key;
10670
10888
  this.testObservability = new testOpsConfig_default(options.testObservability !== false, !isUndefined(options.testObservability));
10671
10889
  this.percy = options.percy || false;
10672
- this.accessibility = options.accessibility !== void 0 ? options.accessibility : null;
10890
+ this.accessibility = options.accessibility;
10673
10891
  this.app = options.app;
10674
10892
  this.appAutomate = !isUndefined(options.app);
10675
10893
  this.automate = !this.appAutomate;
@@ -11336,7 +11554,7 @@ var BrowserstackLauncherService = class {
11336
11554
  _projectName;
11337
11555
  _buildTag;
11338
11556
  _buildIdentifier;
11339
- _accessibilityAutomation = null;
11557
+ _accessibilityAutomation;
11340
11558
  _percy;
11341
11559
  _percyBestPlatformCaps;
11342
11560
  browserStackConfig;
@@ -11476,7 +11694,7 @@ var BrowserstackLauncherService = class {
11476
11694
  this._updateObjectTypeCaps(capabilities, "goog:chromeOptions", overrideOptions);
11477
11695
  }
11478
11696
  if (buildStartResponse?.accessibility) {
11479
- if (this._accessibilityAutomation === null) {
11697
+ if (isUndefined(this._accessibilityAutomation)) {
11480
11698
  this.browserStackConfig.accessibility = buildStartResponse.accessibility.success;
11481
11699
  this._accessibilityAutomation = buildStartResponse.accessibility.success;
11482
11700
  this._options.accessibility = buildStartResponse.accessibility.success;
@@ -11497,6 +11715,7 @@ var BrowserstackLauncherService = class {
11497
11715
  } else if (isAccessibilityAutomationSession(this._accessibilityAutomation)) {
11498
11716
  this._updateObjectTypeCaps(capabilities, "accessibilityOptions", {});
11499
11717
  }
11718
+ this._removeCliOnlyCapabilityOptions(capabilities);
11500
11719
  if (shouldSetupPercy) {
11501
11720
  try {
11502
11721
  const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities);
@@ -11774,13 +11993,40 @@ Visit https://automation.browserstack.com/builds/${process.env[BROWSERSTACK_TEST
11774
11993
  }
11775
11994
  async _uploadServiceLogs() {
11776
11995
  const clientBuildUuid = this._getClientBuildUuid();
11777
- await PerformanceTester.measureWrapper(EVENTS.SDK_UPLOAD_LOGS, async () => {
11778
- const response = await uploadLogs(getBrowserStackUser(this._config), getBrowserStackKey(this._config), clientBuildUuid);
11779
- if (response) {
11780
- BStackLogger2.info(`Upload response: ${JSON.stringify(response, null, 2)}`);
11781
- BStackLogger2.logToFile(`Response - ${format4(response)}`, "debug");
11996
+ const response = await uploadLogs(getBrowserStackUser(this._config), getBrowserStackKey(this._config), clientBuildUuid);
11997
+ if (response) {
11998
+ BStackLogger2.info(`Upload response: ${JSON.stringify(response, null, 2)}`);
11999
+ BStackLogger2.logToFile(`Response - ${format4(response)}`, "debug");
12000
+ }
12001
+ }
12002
+ _removeCliOnlyCapabilityOptions(capabilities) {
12003
+ if (!capabilities || typeof capabilities !== "object") {
12004
+ return;
12005
+ }
12006
+ const strip = (capability) => {
12007
+ const capabilityRecord = capability;
12008
+ const bstackOptions = capabilityRecord["bstack:options"];
12009
+ if (bstackOptions && typeof bstackOptions === "object") {
12010
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach((key) => delete bstackOptions[key]);
11782
12011
  }
11783
- });
12012
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach((key) => delete capabilityRecord[`browserstack.${key}`]);
12013
+ const alwaysMatch = capability.alwaysMatch;
12014
+ if (alwaysMatch && typeof alwaysMatch === "object") {
12015
+ strip(alwaysMatch);
12016
+ }
12017
+ };
12018
+ if (Array.isArray(capabilities)) {
12019
+ capabilities.flatMap((c) => {
12020
+ if (Object.values(c).length > 0 && Object.values(c).every((c2) => typeof c2 === "object" && c2.capabilities)) {
12021
+ return Object.values(c).map((o) => o.capabilities);
12022
+ }
12023
+ return c;
12024
+ }).forEach(strip);
12025
+ } else {
12026
+ Object.entries(capabilities).forEach(([, caps]) => {
12027
+ strip(caps.capabilities);
12028
+ });
12029
+ }
11784
12030
  }
11785
12031
  _updateObjectTypeCaps(capabilities, capType, value) {
11786
12032
  try {
@@ -12571,11 +12817,20 @@ var BrowserstackService = class {
12571
12817
  PerformanceTester.end(EVENTS.SDK_FIND_NEAREST_HUB, false, "Hub URL not found");
12572
12818
  }
12573
12819
  }
12574
- if (BrowserstackCLI.getInstance().isRunning()) {
12575
- await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.CREATE, HookState.PRE, { caps: capabilities });
12576
- const instance = AutomationFramework.getTrackedInstance();
12577
- const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES);
12578
- Object.assign(capabilities, caps);
12820
+ try {
12821
+ if (BrowserstackCLI.getInstance().isRunning()) {
12822
+ await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.CREATE, HookState.PRE, { caps: capabilities });
12823
+ const instance = AutomationFramework.getTrackedInstance();
12824
+ const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES);
12825
+ Object.assign(capabilities, caps);
12826
+ const bstackOptions = capabilities["bstack:options"];
12827
+ if (bstackOptions && typeof bstackOptions === "object") {
12828
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach((key) => delete bstackOptions[key]);
12829
+ }
12830
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach((key) => delete capabilities[`browserstack.${key}`]);
12831
+ }
12832
+ } catch (err) {
12833
+ BStackLogger2.error(`Error while tracking automation framework event: ${err}`);
12579
12834
  }
12580
12835
  } catch (err) {
12581
12836
  BStackLogger2.error(`Error while connecting to Browserstack CLI: ${err}`);
@@ -12752,8 +13007,8 @@ var BrowserstackService = class {
12752
13007
  }
12753
13008
  async afterTest(test, context, results) {
12754
13009
  this._specsRan = true;
12755
- const { error, passed } = results;
12756
- if (!passed) {
13010
+ const { error, passed, skipped } = results;
13011
+ if (!passed && !skipped) {
12757
13012
  const testError = error && error.message || "Unknown Error";
12758
13013
  this._failReasons.push(testError);
12759
13014
  this._pureTestFailReasons.push(testError);