From 12e5c14148eb496783606c65bdc4e202845d1b7e Mon Sep 17 00:00:00 2001 From: Procopis-QG Date: Mon, 14 Sep 2026 18:29:39 +0100 Subject: [PATCH] fix(browserstack): send W3C-only capabilities so bstack:options labels are honoured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BrowserStack's hub classifies a create-session request as legacy JSON Wire as soon as any legacy top-level key (`device`, `os_version`, `app`) is present. In that mode it reads project/build/name from the legacy top-level keys, which agent-device never sets, and ignores `bstack:options` entirely — so every session still landed in "Untitled Project" / "Untitled Build" with an empty name after #2495 carried the `--provider-project` / `--provider-build` / `--provider-session-name` flags through to the provider (#2494). `buildBrowserStackCapabilities` now emits `appium:deviceName`, `appium:platformVersion` and `appium:app` instead, leaving `bstack:options` as the only vendor block. Verified against live App Automate sessions: the same `bstack:options` payload is ignored with the legacy keys present and honoured without them, independent of Appium version. Integration assertions updated to the W3C shape and extended to reject any legacy key at the top level. Co-authored-by: Cursor --- CHANGELOG.md | 8 ++++++++ packages/provider-webdriver/src/browserstack.ts | 15 ++++++++++++--- .../cloud-webdriver-lease-http.test.ts | 5 ++--- .../cloud-webdriver-provider-adapters.test.ts | 11 +++++++---- .../cloud-webdriver-runtime.test.ts | 5 ++--- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7222d4e400..d8eb2bd8e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- Fixed: BrowserStack sessions honour `--provider-project`, `--provider-build`, and + `--provider-session-name`. The capability builder emitted the legacy JSON Wire keys `device`, + `os_version`, and `app` at the top level next to the W3C `bstack:options` block; the hub treats a + request carrying any legacy key as a legacy session, reads the labels from the legacy top-level + `project`/`build`/`name` (never set), and ignores `bstack:options`, so every session landed in + "Untitled Project" / "Untitled Build" with an empty name — even after #2495 carried the flags to + the provider (#2494). The builder now emits `appium:deviceName`, `appium:platformVersion`, and + `appium:app` and no legacy key, verified against live App Automate sessions. - Changed (sessions): the implicit session is now keyed by workspace **and platform**, so one checkout can drive iOS and Android without inventing a `--session` name for every command (#2580). An implicit session was addressed by `cwd::default`, one slot per checkout, and it stayed diff --git a/packages/provider-webdriver/src/browserstack.ts b/packages/provider-webdriver/src/browserstack.ts index 0a1dde4140..ca845ea93f 100644 --- a/packages/provider-webdriver/src/browserstack.ts +++ b/packages/provider-webdriver/src/browserstack.ts @@ -222,14 +222,23 @@ export function createBrowserStackUploadApp( }; } +/** + * Builds the W3C `alwaysMatch` capabilities for a BrowserStack App Automate session. + * + * Every key is either W3C-standard (`platformName`), `appium:`-prefixed, or inside + * `bstack:options`. The legacy JSON Wire keys (`device`, `os_version`, `app`, `project`, `build`, + * `name`) must not appear: when the hub sees any of them it treats the whole request as a legacy + * session and reads the labels from the legacy top-level keys instead of `bstack:options`, so the + * project/build/session names are silently dropped and the session lands in "Untitled Project". + */ export function buildBrowserStackCapabilities( options: BrowserStackCapabilitiesOptions, ): Record { const { 'bstack:options': configuredBstackOptions, ...configured } = options.configured ?? {}; return { - device: options.deviceName, - os_version: options.osVersion, - ...(options.app ? { app: options.app } : {}), + 'appium:deviceName': options.deviceName, + 'appium:platformVersion': options.osVersion, + ...(options.app ? { 'appium:app': options.app } : {}), ...configured, // Merged per key, never assigned: `configured` carrying its own `bstack:options` used to // replace the whole object and silently drop the session/build labels below. diff --git a/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts b/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts index 7dc5168d00..3dd8139dda 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts @@ -107,9 +107,8 @@ test('lease_allocate over HTTP prepares the BrowserStack session end to end', as assert.deepEqual(alwaysMatch(create), { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://preuploaded', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://preuploaded', 'bstack:options': { projectName: 'MyProject', buildName: 'Build-2026-09-11', diff --git a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts index 991b4707f6..fdf18eabc7 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts @@ -131,8 +131,12 @@ test('BrowserStack facade nests device-feature capabilities inside bstack:option locale: 'Fr', networkProfile: '4g-lte-advanced-good', }); - // Vendor capabilities stay nested; only BrowserStack's legacy selectors sit at the top level. + // Vendor capabilities stay nested, and no legacy JSON Wire selector reaches the top level — + // the hub would otherwise treat the request as legacy and ignore bstack:options entirely. assert.equal(alwaysMatch?.deviceOrientation, undefined); + for (const legacyKey of ['device', 'os_version', 'app', 'project', 'build', 'name']) { + assert.equal(alwaysMatch?.[legacyKey], undefined, `legacy key ${legacyKey} leaked`); + } }); }, 15_000); @@ -390,9 +394,8 @@ function assertBrowserStackCalls( alwaysMatch: { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://preuploaded', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://preuploaded', 'bstack:options': { projectName: 'agent-device', buildName: `build-${lease.runId}`, diff --git a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts index 3dd4633731..ca846cd0aa 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts @@ -480,9 +480,8 @@ function assertWebDriverCalls(calls: readonly CloudWebDriverHttpCall[], leaseId: alwaysMatch: { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://app-id', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://app-id', 'bstack:options': { projectName: 'agent-device', buildName: 'run-a',