From de59a89c0c5a697f0d05b46e62de1d42fc7181c3 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 12 Aug 2026 13:25:27 +0530 Subject: [PATCH] fix(SDK-7250): make the config debug dump safe, lossless and opt-out-able Investigated replacing the wdio.conf file capture (#128) with a richer log dump, by stringifying hook functions into `_config data:`. That specific goal is NOT achievable -- see below -- but the surrounding hardening is worth having on its own. What this does: - serializeConfigForLog() replaces `JSON.parse(JSON.stringify(config))`. It keeps RegExp values instead of collapsing them to `{}`, returns '[Circular]' instead of throwing on a circular config, and never throws at all -- the previous call sits in the service constructor with no try/catch, so a circular reference from a plugin or reporter took the constructor down. - Credential scrubbing now covers compound key names. The previous exact-name list (`user`, `username`, `key`, `accesskey`, `password`) could not see `clientSecret`, `client_secret`, `CLIENT_SECRET` or `AWS_SECRET_ACCESS_KEY`, all of which were being written to a log that is uploaded. Basic-auth URLs and inline PEM blocks in string values are scrubbed too. - `_options data:` and `webdriver capabilities data:` went through raw JSON.stringify with no object-level redaction at all; both now use the same serializer. - Adds `disableAutoCaptureLogs` (and BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS), honoured in uploadLogs itself so the detached cleanup rescue is covered, since opting out is exactly what leaves logsUploaded false and arms that rescue. Why the original goal is impossible: @wdio/config ConfigParser.addService does `hook.bind(service)` on every hook, including hooks defined in the user's own config file. Per ECMAScript, a bound function has no source text -- `toString()` returns `function () { [native code] }`. Verified on a real run: every hook in the dump reads `["function () { [native code] }"]`. No serializer can recover hook bodies from the config object, so the log cannot substitute for reading the config file. Co-Authored-By: Claude Opus 5 (1M context) --- .../browserstack-service/src/autoCapture.ts | 30 ++++ .../src/configSerializer.ts | 118 ++++++++++++ .../browserstack-service/src/constants.ts | 45 +++++ .../browserstack-service/src/exitHandler.ts | 3 +- packages/browserstack-service/src/launcher.ts | 18 +- packages/browserstack-service/src/types.ts | 14 ++ packages/browserstack-service/src/util.ts | 11 ++ .../tests/configSerializer.test.ts | 168 ++++++++++++++++++ 8 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 packages/browserstack-service/src/autoCapture.ts create mode 100644 packages/browserstack-service/src/configSerializer.ts create mode 100644 packages/browserstack-service/tests/configSerializer.test.ts diff --git a/packages/browserstack-service/src/autoCapture.ts b/packages/browserstack-service/src/autoCapture.ts new file mode 100644 index 0000000..14b46a6 --- /dev/null +++ b/packages/browserstack-service/src/autoCapture.ts @@ -0,0 +1,30 @@ +import { BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS } from './constants.js' + +/** + * Opt-out for auto-captured debug logs. + * + * The service has uploaded its debug log for a long time, but this change puts the user's + * HOOK SOURCE in it, so an opt-out is warranted for the first time. Name matches the Node + * SDK's `disableAutoCaptureLogs` so the flag means the same thing across BrowserStack SDKs. + * + * Env var as well as the service option, because the detached cleanup process gets no + * options object — and because CI users cannot always edit a committed config. + */ +export function isAutoCaptureLogsDisabled(options?: { disableAutoCaptureLogs?: boolean }): boolean { + if (options?.disableAutoCaptureLogs === true) { + return true + } + return String(process.env[BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS] || '').toLowerCase() === 'true' +} + +/** + * Mirror the service option onto the environment so the opt-out survives into the detached + * cleanup process, which re-runs the log upload with no options object. + */ +export function publishAutoCaptureDisabled(options?: { disableAutoCaptureLogs?: boolean }): boolean { + const disabled = isAutoCaptureLogsDisabled(options) + if (disabled) { + process.env[BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS] = 'true' + } + return disabled +} diff --git a/packages/browserstack-service/src/configSerializer.ts b/packages/browserstack-service/src/configSerializer.ts new file mode 100644 index 0000000..312b1a1 --- /dev/null +++ b/packages/browserstack-service/src/configSerializer.ts @@ -0,0 +1,118 @@ +import { + COMPOUND_SECRET_SUFFIXES_CAMEL, + COMPOUND_SECRET_SUFFIXES_SNAKE, + PEM_BLOCK_REGEX, + PEM_UNTERMINATED_REGEX, + REDACTED_KEYS, + URL_USERINFO_REGEX +} from './constants.js' + +/** + * Safe, lossless-enough serialization of the user's wdio config for the debug log. + * + * The log already carries a config dump, but `JSON.parse(JSON.stringify(config))` loses + * exactly the parts that matter most when triaging: every hook serialises to `null` + * (`before: [null]`), `RegExp` values collapse to `{}`, and a circular reference — which + * plugins and reporters do produce — throws outright, in the service constructor, with no + * try/catch around it. + * + * This replaces that with a replacer that keeps function source, keeps RegExp, survives + * cycles, and scrubs credentials on the way out. + */ + +const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +/* whole-word key match: `key`, `accessKey`, `browserstack.user`, … */ +const WHOLE_WORD_KEY_REGEX = new RegExp( + `^(?:${[...REDACTED_KEYS].sort((a, b) => b.length - a.length).map(escapeRegex).join('|')})$`, + 'i' +) +/* compound key match: `clientSecret` (camelCase) and `client_secret` / `CLIENT_SECRET` */ +const COMPOUND_CAMEL_KEY_REGEX = new RegExp(`^[A-Za-z0-9_$]{0,64}[a-z0-9](?:${COMPOUND_SECRET_SUFFIXES_CAMEL})$`) +const COMPOUND_SNAKE_KEY_REGEX = new RegExp(`^[A-Za-z0-9_$]{0,64}_(?:${COMPOUND_SECRET_SUFFIXES_SNAKE})$`, 'i') + +/** + * Is this config KEY one whose value must never be logged? + * + * Case matters for the camelCase form and not for the snake form, for the same reason as in + * the text scrubber: a capital (`privateKey`) or an explicit `_` (`client_secret`) is what + * separates a real secret name from `hotkey` and `keyword`. + */ +export function isSensitiveKey(key: string): boolean { + if (!key) { + return false + } + return WHOLE_WORD_KEY_REGEX.test(key) + || COMPOUND_CAMEL_KEY_REGEX.test(key) + || COMPOUND_SNAKE_KEY_REGEX.test(key) +} + +/** + * Line-anchored credential scrub, applied to FUNCTION SOURCE. + * + * Hook bodies are real code, so a secret in one is a `const apiKey = '…'` line rather than a + * config key — object-level key redaction cannot see it. Running the line scrubber over the + * stringified source is what makes serialising functions safe at all. + */ +export function redactSensitiveContent(text: string): string { + if (!text) { + return text + } + + const keys = [...REDACTED_KEYS].sort((a, b) => b.length - a.length).map(escapeRegex).join('|') + const wholeWord = new RegExp(`^.*?(? value + .replace(PEM_BLOCK_REGEX, '$1[REDACTED]$2') + .replace(PEM_UNTERMINATED_REGEX, '$1[REDACTED]') + .replace(URL_USERINFO_REGEX, '$1[REDACTED]@') + +/** + * Serialize any config-shaped object for the debug log. Never throws: a serialization + * failure returns a marker string rather than taking down the caller, which today is the + * service constructor. + */ +export function serializeConfigForLog(value: unknown): string { + try { + const seen = new WeakSet() + + return JSON.stringify(value, function (key, raw) { + if (isSensitiveKey(key)) { + return '[REDACTED]' + } + if (typeof raw === 'function') { + // the whole point: `before: [null]` becomes the actual hook source + return redactSensitiveContent(raw.toString()) + } + if (raw instanceof RegExp) { + return raw.toString() + } + if (typeof raw === 'string') { + return redactStringValue(raw) + } + if (typeof raw === 'object' && raw !== null) { + if (seen.has(raw as object)) { + return '[Circular]' + } + seen.add(raw as object) + } + return raw + }) ?? 'undefined' + } catch (error) { + return `[unserializable: ${(error as Error)?.message || String(error)}]` + } +} diff --git a/packages/browserstack-service/src/constants.ts b/packages/browserstack-service/src/constants.ts index 71e2f49..c2a8bf5 100644 --- a/packages/browserstack-service/src/constants.ts +++ b/packages/browserstack-service/src/constants.ts @@ -52,6 +52,51 @@ export const UPLOAD_LOGS_ENDPOINT = 'client-logs/upload' export const PERCY_LOGS_FILE = 'logs/percy.log' +/** + * Credential scrubbing for the debug-log config dump (SDK-7250). + */ + +/* opt-out, mirroring the Node SDK's flag name */ +export const BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS = 'BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS' + +/* + * Word families that make an identifier sensitive when they appear as its SUFFIX. + * Split by case so camelCase requires a capital (`privateKey` vs `hotkey`) and snake_case + * requires an explicit `_` (`client_secret` vs `keyword`). + */ +export const COMPOUND_SECRET_SUFFIXES_CAMEL = 'Key|Token|Secret|Password|Passwd|Credential' +export const COMPOUND_SECRET_SUFFIXES_SNAKE = 'key|token|secret|password|passwd|credential' + +/* + * The body is TEMPERED so it cannot cross a second `-----BEGIN`: an UNTERMINATED block + * would otherwise match through to a later, unrelated block's END marker and replace + * everything in between. Bounded so the scan stays linear. + */ +export const PEM_BLOCK_REGEX = /(-----BEGIN [^-\r\n]+-----)(?:(?!-----BEGIN)[\s\S]){0,65536}?(-----END [^-\r\n]+-----)/g +/* + * A PEM opened but never closed. Runs must be >=20 chars and end at a non-base64 character: + * letters are valid base64, so a looser rule eats ordinary lines like `nextOption: 1`. + */ +export const PEM_UNTERMINATED_REGEX = /(-----BEGIN [^-\r\n]+-----)(?:\r?\n[A-Za-z0-9+/=]{20,200}(?=[^A-Za-z0-9+/=]|$))+/g +/* + * Userinfo in ANY url value. Password half optional so single-token forms + * (`https://ghp_xxx@github.com`) are caught. Quantifiers BOUNDED: the unbounded form was + * measurably quadratic (100 KB took 6.1 s, 4x per doubling). + */ +export const URL_USERINFO_REGEX = /([a-zA-Z][a-zA-Z0-9+.-]{0,64}:\/\/)[^\s/@:]{1,256}(?::[^\s/@]{0,256})?@/g + +/* Keys whose value is never logged. `user`/`key` are WDIO's own credential options. */ +export const REDACTED_KEYS = [ + 'user', 'key', + 'userName', 'accessKey', + 'browserstack.user', 'browserstack.key', + 'browserstack.userName', 'browserstack.accessKey', + 'password', 'proxyPassword', 'proxyUser', 'proxyPass', + 'localProxyUser', 'localProxyPass', 'proxyUrl', + 'authToken', 'apiKey', 'accessToken', 'secret', 'token', + 'customVariables', 'user_data', 'httpProxy', 'httpsProxy' +] + export const PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [ '/session/:sessionId/url', '/session/:sessionId/forward', diff --git a/packages/browserstack-service/src/exitHandler.ts b/packages/browserstack-service/src/exitHandler.ts index 09e6783..09f4009 100644 --- a/packages/browserstack-service/src/exitHandler.ts +++ b/packages/browserstack-service/src/exitHandler.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url' import PerformanceTester from './instrumentation/performance/performance-tester.js' import TestOpsConfig from './testOps/testOpsConfig.js' import { BStackLogger } from './bstackLogger.js' +import { isAutoCaptureLogsDisabled } from './autoCapture.js' import { BrowserstackCLI } from './cli/index.js' import { BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_TESTHUB_UUID, BROWSERSTACK_KILL_SIGNAL } from './constants.js' @@ -109,7 +110,7 @@ export function shouldCallCleanup(config: BrowserStackConfig, isCLIEnabled = fal // A signal-terminated run never reaches onComplete's log upload, leaving the // build with no SDK-log object — rescue it from the detached cleanup process. const clientBuildUuid = process.env[BROWSERSTACK_TESTHUB_UUID] || config.sdkRunID - if (!config.logsUploaded && config.userName && config.accessKey && clientBuildUuid) { + if (!isAutoCaptureLogsDisabled() && !config.logsUploaded && config.userName && config.accessKey && clientBuildUuid) { args.push('--uploadLogs', clientBuildUuid) } diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 00fb918..5f67a51 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -50,6 +50,8 @@ import { validateSkipAppOverride } from './util.js' import CrashReporter from './crash-reporter.js' +import { serializeConfigForLog } from './configSerializer.js' +import { isAutoCaptureLogsDisabled, publishAutoCaptureDisabled } from './autoCapture.js' import { finalizeOrphanedRuns } from './testOps/openRunsJournal.js' import { BStackLogger } from './bstackLogger.js' import { PercyLogger } from './Percy/PercyLogger.js' @@ -120,11 +122,16 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } this.browserStackConfig = BrowserStackConfig.getInstance(_options, _config, capabilities) - BStackLogger.debug(`_options data: ${JSON.stringify(_options)}`) - BStackLogger.debug(`webdriver capabilities data: ${JSON.stringify(capabilities)}`) - const configCopy = JSON.parse(JSON.stringify(_config)) - CrashReporter.recursivelyRedactKeysFromObject(configCopy, ['user', 'username', 'key', 'accesskey', 'password']) - BStackLogger.debug(`_config data: ${JSON.stringify(configCopy)}`) + // Serialized through serializeConfigForLog rather than JSON.stringify: it keeps hook + // SOURCE instead of `[null]`, keeps RegExp instead of `{}`, survives circular configs + // instead of throwing here in the constructor, and scrubs credentials — including the + // compound key names (`clientSecret`, `AWS_SECRET_ACCESS_KEY`) that the previous + // exact-name list could not see, and secrets inside the hook bodies themselves. + if (!isAutoCaptureLogsDisabled(_options)) { + BStackLogger.debug(`_options data: ${serializeConfigForLog(_options)}`) + BStackLogger.debug(`webdriver capabilities data: ${serializeConfigForLog(capabilities)}`) + BStackLogger.debug(`_config data: ${serializeConfigForLog(_config)}`) + } if (Array.isArray(capabilities)) { capabilities .flatMap((c) => { @@ -248,6 +255,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst @PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_PRE_TEST) async onPrepare (config: Options.Testrunner, capabilities: Capabilities.TestrunnerCapabilities | WebdriverIO.Capabilities) { + publishAutoCaptureDisabled(this._options) PerformanceTester.start(PERFORMANCE_SDK_EVENTS.FRAMEWORK_EVENTS.INIT) // skipAppOverride: emit the fixed warning once + handle the 3 edge cases before anything diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index d01b9d8..79e9f9c 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -103,6 +103,20 @@ export interface BrowserstackConfig { * Currently supports testPlanId. */ testManagementOptions?: TestManagementOptions; + /** + * The service uploads its own debug log at the end of a run so BrowserStack support can + * debug issues without asking you to reproduce them. That log contains your resolved wdio + * config, now including the source of your hooks. + * + * Values under known credential keys are removed first on a best-effort basis, along with + * inline PEM blocks and basic-auth URLs, including inside hook bodies. It is key-name + * driven, so a secret stored under an unrecognised name can still be included — if your + * config or hooks hold secrets you would rather not send, set this to true. + * + * Can also be set with the `BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS=true` env var. + * @default false + */ + disableAutoCaptureLogs?: boolean; /** * Set this to true to enable BrowserStack Percy which will take screenshots * and snapshots for your tests run on Browserstack diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 972ce45..030ea2d 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -48,6 +48,7 @@ import { } from './constants.js' import CrashReporter from './crash-reporter.js' import { BStackLogger } from './bstackLogger.js' +import { isAutoCaptureLogsDisabled } from './autoCapture.js' import UsageStats from './testOps/usageStats.js' import TestOpsConfig from './testOps/testOpsConfig.js' import type { StartBinSessionResponse } from './grpc/index.js' @@ -1527,6 +1528,16 @@ export async function uploadLogs(user: string | undefined, key: string | undefin PerformanceTester.start(eventName) try { + // Honour the opt-out here (not just at the call site) so the DETACHED cleanup rescue + // in cleanup.ts — which calls this with no options — is covered too. Opting out is + // exactly what leaves `logsUploaded` false, which is what arms that rescue. + if (isAutoCaptureLogsDisabled()) { + success = false + failure = 'skipped: disableAutoCaptureLogs=true' + BStackLogger.debug('Skipping log upload, auto-capture is disabled') + return + } + if (!user || !key) { success = false failure = 'skipped: missing_credentials' diff --git a/packages/browserstack-service/tests/configSerializer.test.ts b/packages/browserstack-service/tests/configSerializer.test.ts new file mode 100644 index 0000000..384c989 --- /dev/null +++ b/packages/browserstack-service/tests/configSerializer.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it, afterEach } from 'vitest' + +import { isSensitiveKey, redactSensitiveContent, serializeConfigForLog } from '../src/configSerializer.js' +import { isAutoCaptureLogsDisabled, publishAutoCaptureDisabled } from '../src/autoCapture.js' +import { BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS } from '../src/constants.js' + +afterEach(() => { + delete process.env[BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS] +}) + +describe('serializeConfigForLog — recovering what JSON.stringify loses', () => { + it('keeps hook source instead of collapsing it to null', () => { + const config = { + before: function () { + const chai = require('chai') + global.expect = chai.expect + } + } + + const out = JSON.parse(serializeConfigForLog(config)) + + expect(out.before).toContain('chai') + expect(out.before).not.toBe(null) + }) + + it('keeps every hook in a hook ARRAY (wdio merges hooks into arrays)', () => { + const config = { onPrepare: [() => 'first', () => 'second'] } + + const out = JSON.parse(serializeConfigForLog(config)) + + expect(out.onPrepare).toHaveLength(2) + expect(out.onPrepare[0]).toContain('first') + expect(out.onPrepare[1]).toContain('second') + }) + + it('keeps RegExp instead of {}', () => { + expect(JSON.parse(serializeConfigForLog({ testMatch: /\.e2e\.ts$/ })).testMatch) + .toBe('/\\.e2e\\.ts$/') + }) + + it('survives a circular config instead of throwing', () => { + const config: Record = { framework: 'mocha' } + config.self = config + + expect(() => serializeConfigForLog(config)).not.toThrow() + expect(JSON.parse(serializeConfigForLog(config)).self).toBe('[Circular]') + }) + + it('never throws on a value that cannot be serialized', () => { + const config = { bad: { toJSON() { throw new Error('boom') } } } + + expect(() => serializeConfigForLog(config)).not.toThrow() + expect(serializeConfigForLog(config)).toContain('[unserializable') + }) + + it('handles BigInt, Symbol and undefined without dying', () => { + expect(() => serializeConfigForLog({ a: undefined, b: Symbol('x') })).not.toThrow() + expect(serializeConfigForLog({ big: BigInt(1) })).toContain('[unserializable') + }) +}) + +describe('serializeConfigForLog — credential scrubbing', () => { + it('redacts values under sensitive keys, including compound names', () => { + const out = serializeConfigForLog({ + key: 'BSTACK_KEY_LEAK', + accessKey: 'ACCESS_LEAK', + clientSecret: 'CS_LEAK', + client_secret: 'SNAKE_LEAK', + CLIENT_SECRET: 'SCREAMING_LEAK', + AWS_SECRET_ACCESS_KEY: 'AKIA_LEAK' + }) + + for (const leak of ['BSTACK_KEY_LEAK', 'ACCESS_LEAK', 'CS_LEAK', 'SNAKE_LEAK', 'SCREAMING_LEAK', 'AKIA_LEAK']) { + expect(out).not.toContain(leak) + } + }) + + it('redacts secrets INSIDE hook bodies — the reason serialising functions is safe', () => { + const out = serializeConfigForLog({ + before: function () { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const apiKey = 'sk_live_HOOK_LEAK' + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const GITHUB_TOKEN = 'ghp_HOOK_LEAK' + fetch('https://admin:hunterPASS@internal.example.com') + } + }) + + expect(out).not.toContain('sk_live_HOOK_LEAK') + expect(out).not.toContain('ghp_HOOK_LEAK') + expect(out).not.toContain('hunterPASS') + // the rest of the hook must survive, else capturing it is pointless + expect(out).toContain('fetch') + }) + + it('redacts basic-auth in an ordinary string value', () => { + const out = serializeConfigForLog({ baseUrl: 'https://admin:s3cr3t@example.com' }) + + expect(out).not.toContain('s3cr3t') + expect(out).toContain('example.com') + }) + + it('leaves a port-bearing URL and lookalike keys alone', () => { + const out = serializeConfigForLog({ + baseUrl: 'https://example.com:8080/path', + hotkey: 'ctrl+a', + keyword: 'search', + accessibility: true + }) + + expect(out).toContain('https://example.com:8080/path') + expect(out).toContain('ctrl+a') + expect(out).toContain('search') + expect(out).toContain('"accessibility":true') + }) + + it('stays linear on pathological input (ReDoS guard)', () => { + const started = Date.now() + serializeConfigForLog({ baseUrl: `https://${'a'.repeat(200_000)}` }) + expect(Date.now() - started).toBeLessThan(2_000) + }) +}) + +describe('isSensitiveKey', () => { + it('matches credential keys and compound forms', () => { + for (const k of ['key', 'accessKey', 'clientSecret', 'client_secret', 'CLIENT_SECRET', 'AWS_SECRET_ACCESS_KEY']) { + expect(isSensitiveKey(k)).toBe(true) + } + }) + + it('does not match lookalikes', () => { + for (const k of ['hotkey', 'keyword', 'my_secretary', 'accessibility', 'framework']) { + expect(isSensitiveKey(k)).toBe(false) + } + }) +}) + +describe('redactSensitiveContent', () => { + it('scrubs a multi-line PEM without eating the rest', () => { + const out = redactSensitiveContent([ + 'privateKey: `-----BEGIN PRIVATE KEY-----', + 'MIIEvQIBADANsecretbytes', + '-----END PRIVATE KEY-----`', + 'nextOption: 1' + ].join('\n')) + + expect(out).not.toContain('MIIEvQIBADANsecretbytes') + expect(out).toContain('nextOption') + }) +}) + +describe('auto-capture opt-out', () => { + it('is off by default', () => { + expect(isAutoCaptureLogsDisabled({})).toBe(false) + }) + + it('honours the service option and the env var', () => { + expect(isAutoCaptureLogsDisabled({ disableAutoCaptureLogs: true })).toBe(true) + process.env[BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS] = 'TRUE' + expect(isAutoCaptureLogsDisabled({})).toBe(true) + }) + + it('publishes the option onto the env for the detached cleanup process', () => { + publishAutoCaptureDisabled({ disableAutoCaptureLogs: true }) + expect(process.env[BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS]).toBe('true') + expect(isAutoCaptureLogsDisabled()).toBe(true) + }) +})