From 2e76bd3390b9196c7affa5619002c10bd8dbc2e0 Mon Sep 17 00:00:00 2001 From: Bhargavi-BS Date: Wed, 15 Jul 2026 19:53:58 +0530 Subject: [PATCH 1/2] feat(v8): add skipAppOverride service option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of webdriverio/webdriverio#15374 (v8 backport of #15373) to the standalone repo. With skipAppOverride: true the service classifies the session as App Automate even when no app option is set, skips the app upload, and does not inject an appium:app capability — the user supplies the app reference themselves. Conflicting app option is warned about and ignored; skipAppOverride: false with no app fails fast before any session starts. The option is stripped from bstack:options and never forwarded to the hub. Co-Authored-By: Claude Fable 5 --- .changeset/feat-skip-app-override-v8.md | 5 + .../src/cli/modules/automateModule.ts | 12 +- packages/browserstack-service/src/config.ts | 7 +- .../browserstack-service/src/constants.ts | 2 +- .../src/insights-handler.ts | 9 +- packages/browserstack-service/src/launcher.ts | 15 +- packages/browserstack-service/src/service.ts | 7 + packages/browserstack-service/src/types.ts | 11 ++ packages/browserstack-service/src/util.ts | 47 +++++++ .../tests/cli/modules/automateModule.test.ts | 28 +++- .../tests/insights-handler.test.ts | 14 ++ .../tests/service.test.ts | 12 ++ .../tests/skipAppOverride.test.ts | 130 ++++++++++++++++++ 13 files changed, 290 insertions(+), 9 deletions(-) create mode 100644 .changeset/feat-skip-app-override-v8.md create mode 100644 packages/browserstack-service/tests/skipAppOverride.test.ts diff --git a/.changeset/feat-skip-app-override-v8.md b/.changeset/feat-skip-app-override-v8.md new file mode 100644 index 0000000..2595af0 --- /dev/null +++ b/.changeset/feat-skip-app-override-v8.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": minor +--- + +Add a `skipAppOverride` service option for App Automate (Appium) runs (v8). With `skipAppOverride: true` the service classifies the session as App Automate even when no `app` option is set, does not upload an app, and does not inject an `appium:app` capability — the user supplies the app reference themselves (e.g. a pre-uploaded `bs://` hash as a driver capability or via `BROWSERSTACK_APP_ID`). Setting it together with an `app` option logs a conflict warning and ignores the `app` option; `skipAppOverride: false` with no `app` fails fast with a configuration error before any session starts. Unset keeps existing behaviour unchanged. diff --git a/packages/browserstack-service/src/cli/modules/automateModule.ts b/packages/browserstack-service/src/cli/modules/automateModule.ts index 8038860..b5d7b48 100644 --- a/packages/browserstack-service/src/cli/modules/automateModule.ts +++ b/packages/browserstack-service/src/cli/modules/automateModule.ts @@ -7,7 +7,7 @@ import got from 'got' import type { Frameworks, Options } from '@wdio/types' import AutomationFramework from '../frameworks/automationFramework.js' import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js' -import { isBrowserstackSession } from '../../util.js' +import { isBrowserstackSession, isTrue } from '../../util.js' import type TestFrameworkInstance from '../instances/testFrameworkInstance.js' import { TestFrameworkConstants } from '../frameworks/constants/testFrameworkConstants.js' import PerformanceTester from '../../instrumentation/performance/performance-tester.js' @@ -199,7 +199,10 @@ export default class AutomateModule extends BaseModule { async (sessionId: string, sessionName: string, config: { user: string; key: string;}) => { try { const auth = Buffer.from(`${config.user}:${config.key}`).toString('base64') - const isAppAutomate = this.config.app + // skipAppOverride runs App Automate without an app value, so route session + // name/status to the App Automate endpoint on the flag too (config echoed from + // the binary carries skipAppOverride via the binconfig service options). + const isAppAutomate = this.config.app || isTrue(this.config.skipAppOverride) if (isAppAutomate) { this.logger.info('Marking session name for App Automate') } else { @@ -241,7 +244,10 @@ export default class AutomateModule extends BaseModule { async (sessionId: string, sessionStatus: 'passed' | 'failed', sessionErrorMessage: string | undefined, config: { user: string; key: string; }) => { try { const auth = Buffer.from(`${config.user}:${config.key}`).toString('base64') - const isAppAutomate = this.config.app + // skipAppOverride runs App Automate without an app value, so route session + // name/status to the App Automate endpoint on the flag too (config echoed from + // the binary carries skipAppOverride via the binconfig service options). + const isAppAutomate = this.config.app || isTrue(this.config.skipAppOverride) if (isAppAutomate) { this.logger.info('Marking session status for App Automate') } else { diff --git a/packages/browserstack-service/src/config.ts b/packages/browserstack-service/src/config.ts index 74cdf82..a7f5467 100644 --- a/packages/browserstack-service/src/config.ts +++ b/packages/browserstack-service/src/config.ts @@ -1,7 +1,7 @@ import type { AppConfig, BrowserstackConfig } from './types.js' import type { Options } from '@wdio/types' import TestOpsConfig from './testOps/testOpsConfig.js' -import { isUndefined } from './util.js' +import { isTrue, isUndefined } from './util.js' import { v4 as uuidv4 } from 'uuid' import { BStackLogger } from './bstackLogger.js' @@ -40,7 +40,10 @@ class BrowserStackConfig { this.percy = options.percy || false this.accessibility = options.accessibility !== undefined ? options.accessibility : null this.app = options.app - this.appAutomate = !isUndefined(options.app) + // `skipAppOverride: true` marks an App Automate run even when no `app` is set — the user + // supplies the app themselves via the appium:app driver capability, so classification must + // not depend on an app value being present. Mirrors isAppAutomateSession() in the Node SDK. + this.appAutomate = !isUndefined(options.app) || isTrue(options.skipAppOverride) this.automate = !this.appAutomate this.buildIdentifier = options.buildIdentifier this.sdkRunID = uuidv4() diff --git a/packages/browserstack-service/src/constants.ts b/packages/browserstack-service/src/constants.ts index fefa814..3fa6c9b 100644 --- a/packages/browserstack-service/src/constants.ts +++ b/packages/browserstack-service/src/constants.ts @@ -47,7 +47,7 @@ export const BSTACK_SERVICE_VERSION = bstackServiceVersion export const UPDATED_CLI_ENDPOINT = 'sdk/v1/update_cli' export const CLI_STOP_TIMEOUT = 3000 -export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope'] +export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope', 'skipAppOverride'] export const LOGS_FILE = 'logs/bstack-wdio-service.log' export const CLI_DEBUG_LOGS_FILE = 'log/sdk-cli-debug.log' diff --git a/packages/browserstack-service/src/insights-handler.ts b/packages/browserstack-service/src/insights-handler.ts index 5bc5370..767b4d8 100644 --- a/packages/browserstack-service/src/insights-handler.ts +++ b/packages/browserstack-service/src/insights-handler.ts @@ -22,7 +22,8 @@ import { isUndefined, o11yClassErrorHandler, removeAnsiColors, - getObservabilityProduct + getObservabilityProduct, + isTrue } from './util.js' import type { TestData, @@ -82,6 +83,12 @@ class _InsightsHandler { } _isAppAutomate(): boolean { + // Honor skipAppOverride here too: this handler recomputes App Automate from caps for the + // Observability product attribution, but a skipAppOverride run has no injected `appium:app` + // cap, so without this it would misclassify as 'automate'. Mirrors service._isAppAutomate(). + if (isTrue(this._options?.skipAppOverride)) { + return true + } const browserDesiredCapabilities = (this._browser?.capabilities ?? {}) as Capabilities.DesiredCapabilities const desiredCapabilities = (this._userCaps ?? {}) as Capabilities.DesiredCapabilities return !!browserDesiredCapabilities['appium:app'] || !!desiredCapabilities['appium:app'] || !!(( desiredCapabilities as any)['appium:options']?.app) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 28e789b..53414ee 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -45,7 +45,8 @@ import { normalizeTestReportingConfig, normalizeTestReportingEnvVariables, isValidEnabledValue, - isMultiRemoteCaps + isMultiRemoteCaps, + validateSkipAppOverride } from './util.js' import { getProductMap } from './testHub/utils.js' import CrashReporter from './crash-reporter.js' @@ -219,6 +220,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst async onPrepare (config: Options.Testrunner, capabilities: Capabilities.RemoteCapabilities) { PerformanceTester.start(PERFORMANCE_SDK_EVENTS.FRAMEWORK_EVENTS.INIT) + // skipAppOverride: emit the fixed warning once + handle the 3 edge cases before anything + // else. Runs once here in the launcher (main process). Edge-2 (explicit false + no app) is a + // deliberate pre-session config error, surfaced as SevereServiceError so the run aborts cleanly. + try { + validateSkipAppOverride(this._options) + } catch (error) { + throw new SevereServiceError((error as Error).message) + } + // Keep the config singleton consistent: validateSkipAppOverride clears this._options.app on the + // edge-1 conflict, but browserStackConfig.app was copied earlier in the constructor. + this.browserStackConfig.app = this._options.app + // // Send Funnel start request await sendStart(this.browserStackConfig) diff --git a/packages/browserstack-service/src/service.ts b/packages/browserstack-service/src/service.ts index 4a85c19..eb952e9 100644 --- a/packages/browserstack-service/src/service.ts +++ b/packages/browserstack-service/src/service.ts @@ -695,6 +695,13 @@ export default class BrowserstackService implements Services.ServiceInstance { await this._printSessionURL() } _isAppAutomate(): boolean { + // `skipAppOverride: true` is an App Automate run where the app cap may be supplied by the + // user via the `appium:app` driver capability rather than an SDK-injected one. This worker-local + // check has no access to BrowserStackConfig, so honor the option directly — otherwise the + // session mis-routes to the Automate endpoint and a11y/insights misclassify. + if (isTrue(this._options.skipAppOverride)) { + return true + } const browserDesiredCapabilities = (this._browser?.capabilities ?? {}) as Capabilities.DesiredCapabilities const desiredCapabilities = (this._caps ?? {}) as Capabilities.DesiredCapabilities return !!browserDesiredCapabilities['appium:app'] || !!desiredCapabilities['appium:app'] || !!(( desiredCapabilities as any)['appium:options']?.app) diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index 35e377a..fffcea1 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -139,6 +139,17 @@ export interface BrowserstackConfig { * @default undefined */ app?: string | AppConfig; + /** + * Treat this session as App Automate without the SDK managing the app. + * When `true`, the SDK classifies the run as App Automate, does NOT upload + * an app, and does NOT inject an `appium:app` capability — you supply the + * app reference yourself via the `appium:app` driver capability. + * When explicitly `false` and no `app` is provided, the SDK throws a + * config error before the run starts. Leaving it unset preserves existing + * behaviour. + * @default undefined + */ + skipAppOverride?: boolean; /** * Enable routing connections from BrowserStack cloud through your computer. * You will also need to set `browserstack.local` to true in browser capabilities. diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 900b720..e6c84a0 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1365,6 +1365,53 @@ export function isFalse(value?: any) { return (value + '').toLowerCase() === 'false' } +/** + * skipAppOverride validation chokepoint — ported from the Node SDK's + * validateSkipAppOverride (helper.js) / Java BrowserStackJavaAgent.processAppOptions. + * Called ONCE from the launcher (main process, pre-session). Emits the fixed warning, + * handles the three edge cases, and clears the app on conflict so it is neither uploaded + * nor injected. There is NO Tier-2 branch: every WebdriverIO test framework + * (mocha/jasmine/cucumber) supports App Automate. + * + * The standard warning and the edge-2 error are verbatim with the other BrowserStack SDKs + * (BrowserStackJavaAgent#processAppOptions). The edge-1 conflict warning is INTENTIONALLY adapted for + * WebdriverIO — it says "browserstack service options" (where WDIO reads `app`), not "browserstack.yml" — + * so please do NOT "fix" it back to the yml wording to match the other SDKs. + * Graceful: never throws except the deliberate edge-2 config error (explicit false + no app), + * which fires pre-session so the run aborts cleanly. + * + * Mutates `options.app` (edge-1). 3-state via isTrue/isFalse — never `!options.skipAppOverride`. + */ +export function validateSkipAppOverride(options?: BrowserstackConfig) { + if (isUndefined(options)) { + return + } + + const skipAppOverride = (options as BrowserstackConfig).skipAppOverride + const alreadyWarned = isTrue(process.env.BROWSERSTACK_SKIP_APP_OVERRIDE_WARNED) + + // Standard warning — fires whenever skipAppOverride is true, before any edge return. + if (isTrue(skipAppOverride) && !alreadyWarned) { + BStackLogger.warn('[BrowserStack] \'skipAppOverride: true\' is set. The SDK will treat this session as App Automate and will NOT manage app upload or inject app capabilities. If you intended to run an Automate (browser/website) session, remove \'skipAppOverride\' — leaving it set will cause Automate sessions to behave incorrectly.') + process.env.BROWSERSTACK_SKIP_APP_OVERRIDE_WARNED = 'true' + } + + // Edge 1: app specified + skipAppOverride:true → warn, clear the app so it is not + // uploaded/injected, proceed App Automate. + if (isTrue(skipAppOverride) && !isUndefined((options as BrowserstackConfig).app)) { + BStackLogger.warn('Conflict detected. skipAppOverride: true is active; ignoring the app provided in the browserstack service options.') + ;(options as BrowserstackConfig).app = undefined + return + } + + // Edge 2: app not specified + skipAppOverride explicitly false → pre-session config error. + if (isFalse(skipAppOverride) && isUndefined((options as BrowserstackConfig).app)) { + throw new Error('App capability is missing. When skipAppOverride is set to \'false\', a valid app capability (hash/shareable id/path/custom_id) must be provided.') + } + + // Edge 3 (and default): unset + no app → unchanged behavior (no-op). +} + export function frameworkSupportsHook(hook: string, framework?: string) { if (framework === 'mocha' && (hook === 'before' || hook === 'after' || hook === 'beforeEach' || hook === 'afterEach')) { return true diff --git a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts index 61bb432..a5f7621 100644 --- a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' +import got from 'got' import AutomateModule from '../../../src/cli/modules/automateModule.js' import type { Options } from '@wdio/types' @@ -31,7 +32,8 @@ vi.mock('got', () => ({ })) vi.mock('../../../src/util.js', () => ({ - isBrowserstackSession: vi.fn(() => false) + isBrowserstackSession: vi.fn(() => false), + isTrue: vi.fn((value) => (value + '').toLowerCase() === 'true') })) describe('AutomateModule', () => { @@ -124,6 +126,30 @@ describe('AutomateModule', () => { await expect(automateModule.markSessionName(sessionId, sessionName, config)).resolves.toBeUndefined() }) + it('routes markSessionName to the App Automate endpoint when skipAppOverride is true and no app is set', async () => { + (automateModule.config as any).app = undefined + ;(automateModule.config as any).skipAppOverride = true + vi.mocked(got).mockResolvedValue({ body: {} } as any) + + await automateModule.markSessionName('test-session-id', 'test-session-name', { user: 'testuser', key: 'testkey' }) + + expect(got).toHaveBeenCalledWith( + expect.objectContaining({ url: expect.stringContaining('app-automate/sessions') }) + ) + }) + + it('routes markSessionStatus to the App Automate endpoint when skipAppOverride is true and no app is set', async () => { + (automateModule.config as any).app = undefined + ;(automateModule.config as any).skipAppOverride = true + vi.mocked(got).mockResolvedValue({ body: {} } as any) + + await automateModule.markSessionStatus('test-session-id', 'passed', undefined, { user: 'testuser', key: 'testkey' }) + + expect(got).toHaveBeenCalledWith( + expect.objectContaining({ url: expect.stringContaining('app-automate/sessions') }) + ) + }) + it('should handle onBeforeTest with skipSessionName enabled', async () => { const configWithSkip = { ...mockConfig, diff --git a/packages/browserstack-service/tests/insights-handler.test.ts b/packages/browserstack-service/tests/insights-handler.test.ts index 8af3086..a113533 100644 --- a/packages/browserstack-service/tests/insights-handler.test.ts +++ b/packages/browserstack-service/tests/insights-handler.test.ts @@ -73,6 +73,20 @@ it('should initialize correctly', () => { expect(insightsHandler['_framework']).toEqual('framework') }) +describe('_isAppAutomate skipAppOverride', () => { + it('classifies App Automate (product=app-automate) when skipAppOverride is set with no app cap', () => { + const handler = new InsightsHandler(browser, 'framework', undefined, { skipAppOverride: true } as any) + expect(handler._isAppAutomate()).toBe(true) + expect(handler['_platformMeta']?.product).toBe('app-automate') + }) + + it('classifies Automate when skipAppOverride is unset and no app cap is present', () => { + const handler = new InsightsHandler(browser, 'framework', undefined, {} as any) + expect(handler._isAppAutomate()).toBe(false) + expect(handler['_platformMeta']?.product).toBe('automate') + }) +}) + describe('before', () => { const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession') diff --git a/packages/browserstack-service/tests/service.test.ts b/packages/browserstack-service/tests/service.test.ts index 9da68c1..0007f81 100644 --- a/packages/browserstack-service/tests/service.test.ts +++ b/packages/browserstack-service/tests/service.test.ts @@ -1905,3 +1905,15 @@ describe('ignoreHooksStatus feature', () => { }) }) }) + +describe('_isAppAutomate honors skipAppOverride', () => { + it('returns true when skipAppOverride is set even with no appium:app cap', () => { + const svc = new BrowserstackService({ skipAppOverride: true } as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} } as any) + expect(svc._isAppAutomate()).toBe(true) + }) + + it('returns false for a web session with no app and no skipAppOverride', () => { + const svc = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} } as any) + expect(svc._isAppAutomate()).toBe(false) + }) +}) diff --git a/packages/browserstack-service/tests/skipAppOverride.test.ts b/packages/browserstack-service/tests/skipAppOverride.test.ts new file mode 100644 index 0000000..64971c8 --- /dev/null +++ b/packages/browserstack-service/tests/skipAppOverride.test.ts @@ -0,0 +1,130 @@ +import path from 'node:path' +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' + +import * as bstackLogger from '../src/bstackLogger.js' +import { validateSkipAppOverride } from '../src/util.js' +import { NOT_ALLOWED_KEYS_IN_CAPS } from '../src/constants.js' +import BrowserStackConfig from '../src/config.js' + +vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) +vi.mock('uuid', () => ({ v4: () => 'test-uuid' })) +// util.ts imports formdata-node (ESM-only) at module load for the log-upload path; the code under +// test here never uses FormData, so mock it to keep this suite decoupled from that import. +vi.mock('formdata-node', () => ({ FormData: class {}, File: class {} })) +vi.mock('formdata-node/file-from-path', () => ({ fileFromPath: vi.fn() })) +// ESM-only transitive deps dragged in by util.ts's module graph (log upload, cli modules). None are +// exercised by these validator/classification tests — factory-mock them so the graph loads under vitest. +vi.mock('got', () => ({ default: vi.fn() })) +vi.mock('tar', () => ({ default: {}, create: vi.fn(), extract: vi.fn() })) +vi.mock('git-repo-info', () => ({ default: vi.fn(() => ({})) })) + +// Isolate the warning surface — BStackLogger.warn otherwise writes to the log file + wdio logger. +const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn').mockImplementation(() => {}) +vi.spyOn(bstackLogger.BStackLogger, 'logToFile').mockImplementation(() => {}) + +const STANDARD_WARNING = '[BrowserStack] \'skipAppOverride: true\' is set. The SDK will treat this session as App Automate and will NOT manage app upload or inject app capabilities. If you intended to run an Automate (browser/website) session, remove \'skipAppOverride\' — leaving it set will cause Automate sessions to behave incorrectly.' +const CONFLICT_WARNING = 'Conflict detected. skipAppOverride: true is active; ignoring the app provided in the browserstack service options.' +const MISSING_APP_ERROR = 'App capability is missing. When skipAppOverride is set to \'false\', a valid app capability (hash/shareable id/path/custom_id) must be provided.' + +describe('validateSkipAppOverride', () => { + beforeEach(() => { + warnSpy.mockClear() + delete process.env.BROWSERSTACK_SKIP_APP_OVERRIDE_WARNED + }) + afterEach(() => { + delete process.env.BROWSERSTACK_SKIP_APP_OVERRIDE_WARNED + }) + + it('is a no-op when options is undefined', () => { + expect(() => validateSkipAppOverride(undefined)).not.toThrow() + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('emits the standard warning verbatim once when skipAppOverride:true with no app', () => { + const options = { skipAppOverride: true } as any + validateSkipAppOverride(options) + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy).toHaveBeenCalledWith(STANDARD_WARNING) + expect(options.app).toBeUndefined() + }) + + it('coerces the string "true" (3-state semantics)', () => { + validateSkipAppOverride({ skipAppOverride: 'true' } as any) + expect(warnSpy).toHaveBeenCalledWith(STANDARD_WARNING) + }) + + it('edge-1: skipAppOverride:true + app => conflict warning verbatim and app is cleared', () => { + const options = { skipAppOverride: true, app: 'bs://abc' } as any + validateSkipAppOverride(options) + expect(warnSpy).toHaveBeenCalledWith(STANDARD_WARNING) + expect(warnSpy).toHaveBeenCalledWith(CONFLICT_WARNING) + expect(options.app).toBeUndefined() + }) + + it('edge-2: explicit false + no app => throws the exact missing-app error', () => { + expect(() => validateSkipAppOverride({ skipAppOverride: false } as any)).toThrow(MISSING_APP_ERROR) + }) + + it('edge-2: string "false" + no app => throws', () => { + expect(() => validateSkipAppOverride({ skipAppOverride: 'false' } as any)).toThrow(MISSING_APP_ERROR) + }) + + it('explicit false WITH an app => no throw, no warning', () => { + expect(() => validateSkipAppOverride({ skipAppOverride: false, app: 'bs://abc' } as any)).not.toThrow() + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('edge-3: unset + no app => no-op, no warning, no throw', () => { + expect(() => validateSkipAppOverride({} as any)).not.toThrow() + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('emit-once: a second call in the same process does not re-warn', () => { + validateSkipAppOverride({ skipAppOverride: true } as any) + expect(warnSpy).toHaveBeenCalledTimes(1) + warnSpy.mockClear() + validateSkipAppOverride({ skipAppOverride: true } as any) + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('never emits a Tier-2 "does not support App Automate" warning (WDIO supports App Automate)', () => { + validateSkipAppOverride({ skipAppOverride: true } as any) + const messages = warnSpy.mock.calls.map((c) => String(c[0])) + expect(messages.some((m) => m.includes('does not support App Automate'))).toBe(false) + }) +}) + +describe('NOT_ALLOWED_KEYS_IN_CAPS cloud-leak strip', () => { + it('includes skipAppOverride so it is never forwarded to bstack:options', () => { + expect(NOT_ALLOWED_KEYS_IN_CAPS).toContain('skipAppOverride') + }) +}) + +describe('BrowserStackConfig appAutomate honors skipAppOverride', () => { + const baseConfig = { framework: 'mocha', user: 'u', key: 'k' } as any + beforeEach(() => { + (BrowserStackConfig as any)._instance = undefined + }) + + it('marks app_automate when skipAppOverride is true with no app', () => { + const cfg = BrowserStackConfig.getInstance({ skipAppOverride: true } as any, baseConfig) + expect(cfg.appAutomate).toBe(true) + expect(cfg.automate).toBe(false) + }) + + it('marks app_automate for the string "true"', () => { + const cfg = BrowserStackConfig.getInstance({ skipAppOverride: 'true' } as any, baseConfig) + expect(cfg.appAutomate).toBe(true) + }) + + it('keeps automate for a web run (skipAppOverride false, no app)', () => { + const cfg = BrowserStackConfig.getInstance({ skipAppOverride: false } as any, baseConfig) + expect(cfg.appAutomate).toBe(false) + expect(cfg.automate).toBe(true) + }) + + it('still marks app_automate when options.app is set (existing behaviour)', () => { + const cfg = BrowserStackConfig.getInstance({ app: 'bs://abc' } as any, baseConfig) + expect(cfg.appAutomate).toBe(true) + }) +}) From 3d9ae4fd31a4461f4fc4117a5d45c052b5137f1c Mon Sep 17 00:00:00 2001 From: Bhargavi-BS Date: Thu, 16 Jul 2026 18:33:10 +0530 Subject: [PATCH 2/2] chore: remove changeset file (review feedback) Co-Authored-By: Claude Fable 5 --- .changeset/feat-skip-app-override-v8.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/feat-skip-app-override-v8.md diff --git a/.changeset/feat-skip-app-override-v8.md b/.changeset/feat-skip-app-override-v8.md deleted file mode 100644 index 2595af0..0000000 --- a/.changeset/feat-skip-app-override-v8.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@wdio/browserstack-service": minor ---- - -Add a `skipAppOverride` service option for App Automate (Appium) runs (v8). With `skipAppOverride: true` the service classifies the session as App Automate even when no `app` option is set, does not upload an app, and does not inject an `appium:app` capability — the user supplies the app reference themselves (e.g. a pre-uploaded `bs://` hash as a driver capability or via `BROWSERSTACK_APP_ID`). Setting it together with an `app` option logs a conflict warning and ignores the `app` option; `skipAppOverride: false` with no `app` fails fast with a configuration error before any session starts. Unset keeps existing behaviour unchanged.