Skip to content

Release 27 aug - #169

Merged
hamza-browserstack merged 16 commits into
mainfrom
release_27_aug
Aug 27, 2026
Merged

Release 27 aug#169
hamza-browserstack merged 16 commits into
mainfrom
release_27_aug

Conversation

@harshit-browserstack

Copy link
Copy Markdown
Collaborator

What is this about?

Related Jira task/s

Release (mandatory for every PR — required for the ready-for-review label)

Version bump: (required — tick exactly one)

  • minor (backwards-compatible feature)
  • patch (bug fix or other small change)

Release notes type: (optional)

  • New Feature
  • Bug Fix
  • Other Improvement

Release notes (customer-facing): (optional but encouraged)

Release notes (internal): (required — engineer-facing; what actually changed / why)

Checklist

  • Ready to review
  • Has it been tested locally?

PR Validations

Run Tests: Comment RUN_TESTS to trigger sanity tests.

harshit-browserstack and others added 15 commits August 24, 2026 17:20
Follow-up to #148. @wdio/mocha-framework binds beforeTest to the test function
itself (wrapGlobalTestMethod), so onBeforeTest is the single per-test naming
opportunity and it has already passed by the time the test body calls
browser.reloadSession(). The replacement session is never registered in
sessionMap, so flushSessionName() returns early on !sessionData and the
onAfterExecute sweep -- which iterates the same map -- misses it too.
service.onReload learns the new session id but only renames the outgoing one,
leaving the live session on its creation-time sessionName capability.

onAfterTest now re-resolves the live session id and adopts it into sessionMap
before flushing the name, while that session is still open. The naming block is
gated on skipSessionName rather than skipSessionStatus and sits above the status
gate: gating a name repair behind a status flag would skip it for
setSessionStatus:false users, and adopting unconditionally would pull
setSessionName:false users into sessionMap and start issuing them a status PUT
per session where they previously had none.

Adoption also records the test result, so the post-reload session's status is
marked at teardown instead of being dropped by the old `if (sessionData)` guard.
Steady state costs nothing extra -- appliedName de-dupes the flush when the
session did not change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ture aliased configs

Three defects, each reproduced on an App Automate device run and verified against the
session's own appAllyScan telemetry.

App-A11y scanning is gated on a per-session entry that only onBeforeTest and onHookStart
ever wrote, and that entry is keyed on the session id:

- Commands issued from WDIO's config-level before()/beforeSession() ran before any entry
  existed, so a hook that launches the app and signs in produced no scans at all while
  still counting as expected coverage. The gate now opens at driver creation.
- browser.reloadSession() leaves the entry stranded under the old session id, so nothing
  scanned for the rest of the reloading test. onReload now migrates it, on both the CLI
  and the classic handler paths.

Config auto-capture followed only relative import specifiers, so a project that wires its
split configs through tsconfig `paths` uploaded its entry config alone — the file carrying
the hooks was never in the bundle. Aliases are now resolved from the nearest
tsconfig/jsconfig (following `extends`, tolerating JSONC); the *.conf.* name filter still
keeps application source out.

SDK-7422
…replace

CodeQL flagged the substitution as an incomplete replacement. The sharper problem is the
other half of `String.replace` with a string pattern: `$&` / `$'` in the REPLACEMENT are
interpreted, so a specifier carrying those characters resolved to a corrupted path and the
imported config was silently dropped from the bundle.

Slicing at the first `*` is immune to both, and TypeScript allows at most one `*` per
target, so the first is the substitution point by definition. Test pins it — it fails on
the previous implementation, which resolved `@confs/a$&b.conf` to `configs/a*b.conf.ts`.

SDK-7422
Scans fired from WDIO's config-level before() had neither thTestRunUuid (no test exists yet)
nor thHookRunUuid, so App-A11y's lookup of the scan's parent in BTCER had nothing to find —
the scan reached the hub and then belonged to nothing.

The config-level window is now reported as a BEFORE_ALL hook run through the existing
framework path, so the framework mints the uuid, the binary emits HookRunStarted/Finished for
it, and this module's own onHookStart observer stamps it onto the scans. The instance the
tracked hook creates is the one the first test reuses, so the hook also lands parented to a
real test_run_id rather than orphaned.

Opened ON DEMAND, from the scan path only. WDIO gives a service no event for a user's
config-level before() — ConfigParser.addService folds the user's config hooks and every
service's hooks into one config.before array that the runner fires with Promise.all — so
there is nothing to detect, and opening it from the lifecycle would report a hook that never
ran on every build in the fleet. Opening it when a scan needs a parent is exactly the
condition that matters, and it self-limits: verified against a control config with no
config-level hook, which reports only its four real Mocha hooks.

SDK-7422
…hook

Wraps the handlers registered for each WDIO hook that runs with a live driver, so the window a
driver command falls into is observable. WDIO tells a service nothing about the other handlers
in a hook array — ConfigParser folds the user's config hooks and every service's hooks into one
array the runner fires with Promise.all — so patching the array in place is the only way to see
a non-ours handler start and finish.

Logging only; no events, no behaviour change. Sync handlers stay sync so a sync throw is not
converted into a rejection.

Verified: 'before' appears only when the user declares one (control config with no hooks shows
none), while the beforeTest/afterTest entries come from expect-webdriverio's snapshot and
soft-assert services — so 'registered at construction' means the user's hooks plus any service
constructed before ours, not user code alone.

SDK-7422
… instead

Reverts the tsconfig `paths` alias resolution added earlier in this PR, restoring
configCapture to what it was: relative imports only, entry config plus package.json.
Following aliases widened what gets uploaded from a customer's project, and the actual
need — seeing the hook code behind a failure — does not require uploading anything.

Instead the launcher reads the hook sources out of the config file and logs them. The
parsed config cannot supply them: ConfigParser folds every hook into an array as
`hook.bind(service)`, and a bound function stringifies to `[native code]`, so the file
is the only place the bodies survive.

Also records which hooks call reloadSession and publishes the list on the environment as
BROWSERSTACK_HOOKS_WITH_RELOAD_SESSION — a reload inside a hook swaps the session under
the driver, which is the shape behind the scan-gate defect this PR fixes.

Parsing notes: brace matching is lexical, so a `}` inside a string, comment or template
literal does not truncate a body; and the parameter list is skipped by balancing its
parentheses before looking for the body brace, because a typed parameter carries its own
braces (`beforeTest: function (test: { title?: string })`) and taking the first one
captured the signature instead of the hook.

Known limit, unchanged from before: only hooks written in the entry config are found. A
split config that imports its hooks elsewhere yields nothing — the same blind spot the
alias work was aimed at, now without uploading files.

The config-level before() scan fix moves to SDK-7422-config-level-before-hook.

SDK-7422
Hook bodies are customer code and the debug log is uploaded, so the captured source now goes
through redactSensitiveContent — the same routine that guards an uploaded config file.

BStackLogger's own scrub is not enough on its own. Measured against a hook body carrying nine
sensitive shapes, it leaves six readable: authToken, password, clientSecret, a snake_case
AWS secret, URL userinfo and an inline `token:` value. It knows only user/key/userName/accessKey
in a key-value or query-string position.

The identifier scan moved ahead of redaction, which is not cosmetic: redaction replaces the
whole matching line, so `await browser.reloadSession({ userName, accessKey })` — reloading with
fresh credentials, a real pattern — collapses to [REDACTED] and the call would disappear from
the detection. Raw text is scanned for the tracked identifiers, then the stored copy is
redacted; extractUserHookSources returns both, and hooksUsing documents that it can only be as
complete as the text handed to it.

Test fixtures use neutral sentinels rather than realistic secrets — the redactor keys on the
field name, not the value shape, so nothing is lost by not committing token-shaped strings.

SDK-7422
Everything that writes to the uploaded logs leaves this branch, so the PR is the
reloadSession scan-gate fix and nothing else. configCapture, constants and launcher are
identical to main again.

Parked on SDK-7422-hook-source-logging, complete with the redaction fix: reading a
customer's hook bodies into an uploaded log needs its own review, on its own timeline,
not as a rider on a scan-gate fix.

SDK-7422
Review catch: the classic handler's migration ran in both flows, and under the CLI it is
redundant. The handler is constructed either way, but `before(sessionId)` — which records
_sessionId and populates the scan map — runs only in the else branch of the CLI check, so in
the binary flow the object holds nothing to migrate. Worse than redundant, in fact: the old
`_sessionId === null` fallback meant the CLI flow would adopt the new id onto an otherwise
inert handler.

Now an explicit either/or: the CLI flow migrates the AccessibilityModule gate, the classic
flow migrates the handler. The handler also only migrates the session it was actually
tracking, rather than treating null as "adopt this one".

SDK-7422
Five review findings, all the same underlying shape: the scan gate is read per command from
framework state, which a reload updates, while several writers captured the session id once
and kept writing to it.

Migrating the gate turned that latent disagreement into a live one. Before the migration both
sides were stale, so they agreed and nothing scanned; after it, commandWrapper reads the new
key while a captured writer sets the old one. Concretely: browser.stopA11yScanning() called
AFTER a reload set false on the dead key while the live key stayed true, so scanning carried
on against an explicit instruction, and startA11yScanning() was a silent no-op — in exactly
the window this PR exists to fix.

- accessibilityModule: the scanning toggles and the results getters resolve the session at
  call time via currentSessionId(). getAccessibilityResults/Summary previously reported the
  PRE-reload session's results, while the classic path reported the live one, so the two flows
  disagreed on the same operation.
- accessibility-handler: the toggles write this._sessionId, which commandWrapper reads and
  this PR migrates, instead of the id captured in before().
- service: one _isCliAccessibilityFlow() predicate, used both where AccessibilityHandler.before()
  is gated and where the reload migration chooses a path. isRunning() alone is NOT equivalent —
  with the binary up against a non-BrowserStack provider, before() DID run, so the handler holds
  the live state and must still be migrated.
- accessibilityMap / LOG_DISABLED_SHOWN are Map<string, boolean>: session ids are strings, the
  Map<number> declaration was a mis-declaration, and the casts it forced (including `as never`
  in the tests) are gone.

Tests: the CLI branch's positive path is now asserted through the module registry rather than
left to an optional chain; the non-BrowserStack-provider case is covered; and both toggle paths
are pinned against the post-reload inversion.

SDK-7422
The predicate added in cec234c landed between the decorator and the method, so the decoration
moved to it: onReload stopped emitting its SDK_HOOK measurement, and the predicate started
reporting one under hookType 'onReload' on every call — including from the before hook, so every
session reported an onReload measurement whether or not it ever reloaded.

Behaviour was unaffected (PerformanceTester.measure returns the raw value on the synchronous
path), which is why nothing surfaced it: valid TypeScript, lint has no opinion, and the unit
suites mock Measure into a pass-through so the binding has no observable behaviour to assert.
Hence a structural test over the decorated files instead — verified to fail with the mistake
reintroduced and pass without it.

Doc comment corrected too: the two accessibility paths are NOT strictly exclusive. The module is
registered on startBinResponse.accessibility?.success alone, independent of provider, so under a
non-BrowserStack provider it can hold real state while the classic handler's before() has also
run. What the predicate decides is which one owns the CLASSIC handler's state.

SDK-7422
@hamza-browserstack
hamza-browserstack merged commit 64e75cf into main Aug 27, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants