@wdio/browserstack-service 9.27.0 → 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.26.1",
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,12 +70,11 @@ 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",
77
- tar: "^7.5.7",
78
- undici: "^6.21.3",
76
+ tar: "^7.5.11",
77
+ undici: "^6.24.0",
79
78
  uuid: "^11.1.0",
80
79
  webdriverio: "workspace:*",
81
80
  "winston-transport": "^4.5.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(),
@@ -2131,8 +2235,17 @@ var init_crash_reporter = __esm({
2131
2235
  for (const prop in obj) {
2132
2236
  if (keys.includes(prop.toLowerCase())) {
2133
2237
  obj[prop] = "[REDACTED]";
2134
- } else if (typeof obj[prop] === "object") {
2238
+ } else if (typeof obj[prop] === "object" && obj[prop] !== null) {
2135
2239
  this.recursivelyRedactKeysFromObject(obj[prop], keys);
2240
+ } else if (typeof obj[prop] === "string") {
2241
+ try {
2242
+ const parsed = JSON.parse(obj[prop]);
2243
+ if (typeof parsed === "object" && parsed !== null) {
2244
+ this.recursivelyRedactKeysFromObject(parsed, keys);
2245
+ obj[prop] = JSON.stringify(parsed);
2246
+ }
2247
+ } catch {
2248
+ }
2136
2249
  }
2137
2250
  }
2138
2251
  }
@@ -2608,10 +2721,8 @@ import path5 from "node:path";
2608
2721
  import util3 from "node:util";
2609
2722
  import gitRepoInfo from "git-repo-info";
2610
2723
  import gitconfig from "gitconfiglocal";
2611
- import { FormData as FormData2 } from "formdata-node";
2612
2724
  import { performance as performance2 } from "node:perf_hooks";
2613
2725
  import { create } from "tar";
2614
- import { fileFromPath } from "formdata-node/file-from-path";
2615
2726
  function getBrowserDescription(cap) {
2616
2727
  cap = cap || {};
2617
2728
  if (cap["bstack:options"]) {
@@ -3181,6 +3292,25 @@ function getObservabilityBuildTags(options, bstackBuildTag) {
3181
3292
  }
3182
3293
  return [];
3183
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
+ }
3184
3314
  function getBrowserStackUser(config) {
3185
3315
  if (process.env.BROWSERSTACK_USERNAME) {
3186
3316
  return process.env.BROWSERSTACK_USERNAME;
@@ -3226,8 +3356,14 @@ function getFailureObject(error) {
3226
3356
  };
3227
3357
  }
3228
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);
3229
3363
  try {
3230
3364
  if (!user || !key) {
3365
+ success = false;
3366
+ failure = "skipped: missing_credentials";
3231
3367
  BStackLogger2.debug("Uploading logs failed due to no credentials");
3232
3368
  return;
3233
3369
  }
@@ -3239,10 +3375,20 @@ async function uploadLogs(user, key, clientBuildUuid) {
3239
3375
  CLI_DEBUG_LOGS_FILE
3240
3376
  ].filter((f) => fs6.existsSync(f));
3241
3377
  const copiedFileNames = [];
3378
+ const archiveAddFailures = [];
3242
3379
  for (const f of filesToArchive) {
3243
- const dest = path5.join(tmpDir, path5.basename(f));
3244
- fs6.copyFileSync(f, dest);
3245
- 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);
3246
3392
  }
3247
3393
  await create(
3248
3394
  {
@@ -3261,8 +3407,8 @@ async function uploadLogs(user, key, clientBuildUuid) {
3261
3407
  dest.on("finish", resolve);
3262
3408
  dest.on("error", reject);
3263
3409
  });
3264
- const formData = new FormData2();
3265
- const file = await fileFromPath(tarGzPath);
3410
+ const formData = new FormData();
3411
+ const file = await fs6.openAsBlob(tarGzPath, { type: "application/x-gzip" });
3266
3412
  formData.append("data", file, "logs.tar.gz");
3267
3413
  formData.append("clientBuildUuid", clientBuildUuid);
3268
3414
  const auth = Buffer.from(`${user}:${key}`).toString("base64");
@@ -3289,10 +3435,21 @@ async function uploadLogs(user, key, clientBuildUuid) {
3289
3435
  if (fs6.existsSync(CLI_DEBUG_LOGS_FILE)) {
3290
3436
  fs6.unlinkSync(CLI_DEBUG_LOGS_FILE);
3291
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
+ }
3292
3445
  return response;
3293
3446
  } catch (error) {
3447
+ success = false;
3448
+ failure = `uploadLogs exception: ${getErrorString(error)}`;
3294
3449
  BStackLogger2.error(`Error while uploading logs: ${getErrorString(error)}`);
3295
3450
  return null;
3451
+ } finally {
3452
+ PerformanceTester.end(eventName, success, failure);
3296
3453
  }
3297
3454
  }
3298
3455
  function truncateString(field, truncateSizeInBytes) {
@@ -3689,7 +3846,10 @@ var init_util = __esm({
3689
3846
  },
3690
3847
  product_map: getProductMapForBuildStartCall(bStackConfig, accessibilityAutomation),
3691
3848
  config: {},
3692
- test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {}
3849
+ test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {},
3850
+ test_management: {
3851
+ test_plan_id: getTestPlanId(options)
3852
+ }
3693
3853
  };
3694
3854
  if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)) {
3695
3855
  data.accessibility.settings ??= {};
@@ -3718,6 +3878,7 @@ var init_util = __esm({
3718
3878
  const jsonResponse = await response.json();
3719
3879
  delete data?.accessibility?.settings?.includeEncodedExtension;
3720
3880
  BStackLogger2.debug(`[Start_Build] Success response: ${JSON.stringify(jsonResponse)}`);
3881
+ BStackLogger2.debug(`Test Plan Id sent in request: ${getTestPlanId(options)}`);
3721
3882
  process.env[TESTOPS_BUILD_COMPLETED_ENV] = "true";
3722
3883
  if (jsonResponse.jwt) {
3723
3884
  process.env[BROWSERSTACK_TESTHUB_JWT] = jsonResponse.jwt;
@@ -4113,14 +4274,18 @@ var init_bstackLogger = __esm({
4113
4274
  static logFilePath = path6.join(process.cwd(), LOGS_FILE);
4114
4275
  static logFolderPath = path6.join(process.cwd(), "logs");
4115
4276
  static logFileStream;
4277
+ static redactCredentials(logMessage) {
4278
+ return logMessage.replace(/(["']?(?:username|userName|accesskey|accessKey|user|key)["']?\s*[:=]\s*["']?)([^"'\s,}]+)/gi, "$1").replace(/([?&](?:username|userName|access_key|accesskey|accessKey|user|key)=)([^&#\s]+)/gi, "$1");
4279
+ }
4116
4280
  static logToFile(logMessage, logLevel) {
4117
4281
  try {
4282
+ const redactedMessage = this.redactCredentials(logMessage);
4118
4283
  if (!this.logFileStream) {
4119
4284
  this.ensureLogsFolder();
4120
4285
  this.logFileStream = fs7.createWriteStream(this.logFilePath, { flags: "a" });
4121
4286
  }
4122
4287
  if (this.logFileStream && this.logFileStream.writable) {
4123
- this.logFileStream.write(this.formatLog(logMessage, logLevel));
4288
+ this.logFileStream.write(this.formatLog(redactedMessage, logLevel));
4124
4289
  }
4125
4290
  } catch (error) {
4126
4291
  log2.debug(`Failed to log to file. Error ${error}`);
@@ -4131,28 +4296,33 @@ var init_bstackLogger = __esm({
4131
4296
  `;
4132
4297
  }
4133
4298
  static info(message) {
4134
- this.logToFile(message, "info");
4135
- log2.info(message);
4299
+ const redactedMessage = this.redactCredentials(message);
4300
+ this.logToFile(redactedMessage, "info");
4301
+ log2.info(redactedMessage);
4136
4302
  }
4137
4303
  static error(message) {
4138
- this.logToFile(message, "error");
4139
- log2.error(message);
4304
+ const redactedMessage = this.redactCredentials(message);
4305
+ this.logToFile(redactedMessage, "error");
4306
+ log2.error(redactedMessage);
4140
4307
  }
4141
4308
  static debug(message, param) {
4142
- this.logToFile(message, "debug");
4309
+ const redactedMessage = this.redactCredentials(message);
4310
+ this.logToFile(redactedMessage, "debug");
4143
4311
  if (param) {
4144
- log2.debug(message, param);
4312
+ log2.debug(redactedMessage, param);
4145
4313
  } else {
4146
- log2.debug(message);
4314
+ log2.debug(redactedMessage);
4147
4315
  }
4148
4316
  }
4149
4317
  static warn(message) {
4150
- this.logToFile(message, "warn");
4151
- log2.warn(message);
4318
+ const redactedMessage = this.redactCredentials(message);
4319
+ this.logToFile(redactedMessage, "warn");
4320
+ log2.warn(redactedMessage);
4152
4321
  }
4153
4322
  static trace(message) {
4154
- this.logToFile(message, "trace");
4155
- log2.trace(message);
4323
+ const redactedMessage = this.redactCredentials(message);
4324
+ this.logToFile(redactedMessage, "trace");
4325
+ log2.trace(redactedMessage);
4156
4326
  }
4157
4327
  static clearLogger() {
4158
4328
  if (this.logFileStream) {
@@ -4183,13 +4353,14 @@ var init_utils = __esm({
4183
4353
  init_bstackLogger();
4184
4354
  init_util();
4185
4355
  getProductMap = (config) => {
4186
- return {
4187
- observability: config.testObservability.enabled,
4188
- accessibility: config.accessibility,
4189
- percy: config.percy,
4190
- automate: config.automate,
4191
- app_automate: config.appAutomate
4192
- };
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));
4193
4364
  };
4194
4365
  shouldProcessEventForTesthub = (eventType) => {
4195
4366
  if (isTrue(process.env[BROWSERSTACK_OBSERVABILITY])) {
@@ -4237,13 +4408,14 @@ var init_utils = __esm({
4237
4408
  }
4238
4409
  };
4239
4410
  getProductMapForBuildStartCall = (config, accessibilityAutomation) => {
4240
- return {
4241
- observability: config.testObservability.enabled,
4242
- accessibility: accessibilityAutomation,
4243
- percy: config.percy,
4244
- automate: config.automate,
4245
- app_automate: config.appAutomate
4246
- };
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));
4247
4419
  };
4248
4420
  }
4249
4421
  });
@@ -4341,7 +4513,7 @@ import {
4341
4513
  DriverInitRequestConstructor,
4342
4514
  FetchDriverExecuteParamsEventRequestConstructor
4343
4515
  } from "@browserstack/wdio-browserstack-service";
4344
- var GrpcClient;
4516
+ var GRPC_MESSAGE_LIMIT, GrpcClient;
4345
4517
  var init_grpcClient = __esm({
4346
4518
  "src/cli/grpcClient.ts"() {
4347
4519
  "use strict";
@@ -4349,6 +4521,7 @@ var init_grpcClient = __esm({
4349
4521
  init_performance_tester();
4350
4522
  init_constants2();
4351
4523
  init_cliLogger();
4524
+ GRPC_MESSAGE_LIMIT = 20 * 1024 * 1024;
4352
4525
  GrpcClient = class _GrpcClient {
4353
4526
  static #instance = null;
4354
4527
  binSessionId;
@@ -4410,17 +4583,20 @@ var init_grpcClient = __esm({
4410
4583
  if (!listenAddress) {
4411
4584
  throw new Error("Unable to determine gRPC server listen address");
4412
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
+ };
4413
4591
  this.channel = new grpcChannel(
4414
4592
  listenAddress,
4415
4593
  grpcCredentials.createInsecure(),
4416
- {
4417
- "grpc.keepalive_time_ms": 1e4
4418
- }
4594
+ channelOptions
4419
4595
  );
4420
4596
  this.client = new SDKClient(
4421
4597
  listenAddress,
4422
4598
  grpcCredentials.createInsecure(),
4423
- {}
4599
+ channelOptions
4424
4600
  );
4425
4601
  this.logger.info(`Connected to gRPC server at ${listenAddress}`);
4426
4602
  }
@@ -5660,12 +5836,12 @@ var init_automateModule = __esm({
5660
5836
  async onAfterTest(args) {
5661
5837
  this.logger.debug("onAfterTest: inside automate module after test hook!");
5662
5838
  const instace = args.instance;
5663
- const { error, passed } = args.result;
5839
+ const { error, passed, skipped } = args.result;
5664
5840
  const _failReasons = [];
5665
- if (!passed) {
5841
+ if (!passed && !skipped) {
5666
5842
  _failReasons.push(error && error.message || "Unknown Error");
5667
5843
  }
5668
- const status = passed ? "passed" : "failed";
5844
+ const status = passed || skipped ? "passed" : "failed";
5669
5845
  const reason = _failReasons.length > 0 ? _failReasons.join("\n") : void 0;
5670
5846
  const autoInstance = AutomationFramework.getTrackedInstance();
5671
5847
  const sessionId = AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID);
@@ -6063,13 +6239,17 @@ var init_wdioMochaTestFramework = __esm({
6063
6239
  }
6064
6240
  loadTestResult(instance, args) {
6065
6241
  const results = args.result;
6066
- const { error, passed } = results;
6242
+ const { error, passed, skipped } = results;
6067
6243
  let result = "passed";
6068
6244
  let failure = null;
6069
6245
  let failureReason = null;
6070
6246
  let failureType = null;
6071
6247
  if (!passed) {
6072
- 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
+ }
6073
6253
  if (error && result !== "skipped") {
6074
6254
  failure = [{ backtrace: [removeAnsiColors(error.message), removeAnsiColors(error.stack || "")] }];
6075
6255
  failureReason = removeAnsiColors(error.message);
@@ -9049,6 +9229,7 @@ var init_cli = __esm({
9049
9229
  init_accessibilityModule();
9050
9230
  init_util();
9051
9231
  init_observabilityModule();
9232
+ init_crash_reporter();
9052
9233
  init_percyModule();
9053
9234
  init_apiUtils();
9054
9235
  BrowserstackCLI = class _BrowserstackCLI {
@@ -9125,7 +9306,9 @@ var init_cli = __esm({
9125
9306
  await this.start();
9126
9307
  this.logger.debug("startMain: main-process started");
9127
9308
  const response = await GrpcClient.getInstance().startBinSession(this.wdioConfig);
9128
- BStackLogger.debug(`start: startBinSession response=${JSON.stringify(response)}`);
9309
+ const redactedStartResponse = JSON.parse(JSON.stringify(response));
9310
+ CrashReporter.recursivelyRedactKeysFromObject(redactedStartResponse, ["user", "username", "key", "accesskey", "password"]);
9311
+ BStackLogger.debug(`start: startBinSession response=${JSON.stringify(redactedStartResponse)}`);
9129
9312
  this.loadModules(response);
9130
9313
  this.isMainConnected = true;
9131
9314
  }
@@ -9158,6 +9341,17 @@ var init_cli = __esm({
9158
9341
  }
9159
9342
  this.modules[ObservabilityModule.MODULE_NAME] = new ObservabilityModule(startBinResponse.observability);
9160
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
+ }
9161
9355
  this.modules[TestHubModule.MODULE_NAME] = new TestHubModule(startBinResponse.testhub);
9162
9356
  if (startBinResponse.accessibility?.success) {
9163
9357
  process.env[BROWSERSTACK_ACCESSIBILITY] = "true";
@@ -9202,12 +9396,61 @@ var init_cli = __esm({
9202
9396
  return Promise.resolve();
9203
9397
  }
9204
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
+ }
9205
9402
  const cmd = [SDK_CLI_BIN_PATH, "sdk"];
9206
9403
  this.logger.debug(`spawning command='${cmd}'`);
9207
- this.process = spawn3(cmd[0], cmd.slice(1), {
9208
- env: process.env
9209
- });
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) {
9210
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
+ }
9211
9454
  const cliOut = {};
9212
9455
  this.process.stdout.on("data", (data) => {
9213
9456
  const lines = data.toString().trim().split("\n");
@@ -9220,8 +9463,7 @@ var init_cli = __esm({
9220
9463
  }
9221
9464
  if (line.toLowerCase().includes("ready")) {
9222
9465
  this.loadCliParams(cliOut);
9223
- PerformanceTester.end(EVENTS.SDK_CLI_START);
9224
- resolve();
9466
+ settle(resolve);
9225
9467
  return;
9226
9468
  }
9227
9469
  }
@@ -9230,14 +9472,11 @@ var init_cli = __esm({
9230
9472
  this.logger.error(`CLI stderr: ${data.toString().trim()}`);
9231
9473
  });
9232
9474
  this.process.on("error", (err) => {
9233
- cliOut.error = `Error in start: ${err.message}`;
9234
- PerformanceTester.end(EVENTS.SDK_CLI_START, false, err);
9235
- reject(new Error(`Failed to start CLI process: ${err.message}`));
9475
+ settle(reject, err);
9236
9476
  });
9237
9477
  this.process.on("close", (code) => {
9238
- if (code !== 0) {
9239
- reject(new Error(`CLI process exited with code ${code}`));
9240
- }
9478
+ const msg = code !== 0 ? `CLI process exited with code ${code}` : "CLI process exited cleanly before emitting ready";
9479
+ settle(reject, new Error(msg));
9241
9480
  });
9242
9481
  });
9243
9482
  }
@@ -9317,7 +9556,9 @@ var init_cli = __esm({
9317
9556
  this.logger.info(`Starting as child process with session ID: ${binSessionId}`);
9318
9557
  GrpcClient.getInstance().connect();
9319
9558
  const response = await GrpcClient.getInstance().connectBinSession();
9320
- this.logger.info(`Connected to bin session: ${JSON.stringify(response)}`);
9559
+ const redactedResponse = JSON.parse(JSON.stringify(response));
9560
+ CrashReporter.recursivelyRedactKeysFromObject(redactedResponse, ["user", "username", "key", "accesskey", "password"]);
9561
+ this.logger.info(`Connected to bin session: ${JSON.stringify(redactedResponse)}`);
9321
9562
  this.loadModules(response);
9322
9563
  this.isChildConnected = true;
9323
9564
  PerformanceTester.end(EVENTS.SDK_CONNECT_BIN_SESSION);
@@ -9385,7 +9626,9 @@ var init_cli = __esm({
9385
9626
  setConfig(response) {
9386
9627
  try {
9387
9628
  this.config = JSON.parse(response.config);
9388
- this.logger.debug(`loadModules: config=${JSON.stringify(this.config)}`);
9629
+ const redactedConfig = JSON.parse(response.config);
9630
+ CrashReporter.recursivelyRedactKeysFromObject(redactedConfig, ["user", "username", "key", "accesskey", "password"]);
9631
+ this.logger.debug(`loadModules: config=${JSON.stringify(redactedConfig)}`);
9389
9632
  } catch (error) {
9390
9633
  this.logger.error(`setConfig: error=${util10.format(error)}`);
9391
9634
  }
@@ -10644,7 +10887,7 @@ var BrowserStackConfig = class _BrowserStackConfig {
10644
10887
  this.accessKey = config.key;
10645
10888
  this.testObservability = new testOpsConfig_default(options.testObservability !== false, !isUndefined(options.testObservability));
10646
10889
  this.percy = options.percy || false;
10647
- this.accessibility = options.accessibility !== void 0 ? options.accessibility : null;
10890
+ this.accessibility = options.accessibility;
10648
10891
  this.app = options.app;
10649
10892
  this.appAutomate = !isUndefined(options.app);
10650
10893
  this.automate = !this.appAutomate;
@@ -11215,7 +11458,7 @@ var BrowserstackLauncherService = class {
11215
11458
  BStackLogger2.debug(`_options data: ${JSON.stringify(_options)}`);
11216
11459
  BStackLogger2.debug(`webdriver capabilities data: ${JSON.stringify(capabilities)}`);
11217
11460
  const configCopy = JSON.parse(JSON.stringify(_config));
11218
- CrashReporter.recursivelyRedactKeysFromObject(configCopy, ["user", "key", "accesskey", "password"]);
11461
+ CrashReporter.recursivelyRedactKeysFromObject(configCopy, ["user", "username", "key", "accesskey", "password"]);
11219
11462
  BStackLogger2.debug(`_config data: ${JSON.stringify(configCopy)}`);
11220
11463
  if (Array.isArray(capabilities)) {
11221
11464
  capabilities.flatMap((c) => {
@@ -11311,7 +11554,7 @@ var BrowserstackLauncherService = class {
11311
11554
  _projectName;
11312
11555
  _buildTag;
11313
11556
  _buildIdentifier;
11314
- _accessibilityAutomation = null;
11557
+ _accessibilityAutomation;
11315
11558
  _percy;
11316
11559
  _percyBestPlatformCaps;
11317
11560
  browserStackConfig;
@@ -11451,7 +11694,7 @@ var BrowserstackLauncherService = class {
11451
11694
  this._updateObjectTypeCaps(capabilities, "goog:chromeOptions", overrideOptions);
11452
11695
  }
11453
11696
  if (buildStartResponse?.accessibility) {
11454
- if (this._accessibilityAutomation === null) {
11697
+ if (isUndefined(this._accessibilityAutomation)) {
11455
11698
  this.browserStackConfig.accessibility = buildStartResponse.accessibility.success;
11456
11699
  this._accessibilityAutomation = buildStartResponse.accessibility.success;
11457
11700
  this._options.accessibility = buildStartResponse.accessibility.success;
@@ -11472,6 +11715,7 @@ var BrowserstackLauncherService = class {
11472
11715
  } else if (isAccessibilityAutomationSession(this._accessibilityAutomation)) {
11473
11716
  this._updateObjectTypeCaps(capabilities, "accessibilityOptions", {});
11474
11717
  }
11718
+ this._removeCliOnlyCapabilityOptions(capabilities);
11475
11719
  if (shouldSetupPercy) {
11476
11720
  try {
11477
11721
  const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities);
@@ -11749,13 +11993,40 @@ Visit https://automation.browserstack.com/builds/${process.env[BROWSERSTACK_TEST
11749
11993
  }
11750
11994
  async _uploadServiceLogs() {
11751
11995
  const clientBuildUuid = this._getClientBuildUuid();
11752
- await PerformanceTester.measureWrapper(EVENTS.SDK_UPLOAD_LOGS, async () => {
11753
- const response = await uploadLogs(getBrowserStackUser(this._config), getBrowserStackKey(this._config), clientBuildUuid);
11754
- if (response) {
11755
- BStackLogger2.info(`Upload response: ${JSON.stringify(response, null, 2)}`);
11756
- 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]);
11757
12011
  }
11758
- });
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
+ }
11759
12030
  }
11760
12031
  _updateObjectTypeCaps(capabilities, capType, value) {
11761
12032
  try {
@@ -12546,11 +12817,20 @@ var BrowserstackService = class {
12546
12817
  PerformanceTester.end(EVENTS.SDK_FIND_NEAREST_HUB, false, "Hub URL not found");
12547
12818
  }
12548
12819
  }
12549
- if (BrowserstackCLI.getInstance().isRunning()) {
12550
- await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.CREATE, HookState.PRE, { caps: capabilities });
12551
- const instance = AutomationFramework.getTrackedInstance();
12552
- const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES);
12553
- 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}`);
12554
12834
  }
12555
12835
  } catch (err) {
12556
12836
  BStackLogger2.error(`Error while connecting to Browserstack CLI: ${err}`);
@@ -12727,8 +13007,8 @@ var BrowserstackService = class {
12727
13007
  }
12728
13008
  async afterTest(test, context, results) {
12729
13009
  this._specsRan = true;
12730
- const { error, passed } = results;
12731
- if (!passed) {
13010
+ const { error, passed, skipped } = results;
13011
+ if (!passed && !skipped) {
12732
13012
  const testError = error && error.message || "Unknown Error";
12733
13013
  this._failReasons.push(testError);
12734
13014
  this._pureTestFailReasons.push(testError);