Skip to content

fix(app-a11y): keep scanning across browser.reloadSession() - #165

Merged
hamza-browserstack merged 13 commits into
mainfrom
SDK-7422-app-a11y-scan-registration
Aug 27, 2026
Merged

fix(app-a11y): keep scanning across browser.reloadSession()#165
hamza-browserstack merged 13 commits into
mainfrom
SDK-7422-app-a11y-scan-registration

Conversation

@kamal-kaur04

@kamal-kaur04 kamal-kaur04 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What is this about?

browser.reloadSession() no longer stops accessibility scanning. That is the whole PR — 125 lines across three source files.

A reload hands the worker a new session id while the driver object, the wrapped commands and the running test all stay the same. The scan gate is keyed on the session id, so the entry registered for the old id is orphaned the moment the reload lands: every command for the rest of that test looks up a key that no longer exists and is silently not scanned. onReload already updated KEY_FRAMEWORK_SESSION_ID; it now carries the gate entry across with it.

Migrating the entry is only half of it: several writers had captured the session id once and kept writing to it, while commandWrapper re-reads framework state per command. Before the migration both sides were stale, so they agreed and nothing scanned; after it they would disagree — a stopA11yScanning() called after a reload would set the dead key while the live key stayed true, so scanning would carry on against an explicit instruction. So the scanning toggles and the results getters now resolve the session at call time. (Reproduced — 3 scans after a post-reload stop — and fixed — 0.)

The classic (accessibility-handler) path had the same staleness — _sessionId is captured once in before() and never updated, so every later scan-gate lookup and results call addressed a session that had ended — so it is migrated too.

Blast radius, measured

The loss is bounded to the test that called reloadSession(), not the session. The next test's onBeforeTest / onHookStart re-registers under the new id, so scanning resumes on its own from the following before each. Worth stating precisely because an earlier revision of this description implied wider damage than the logs support.

Verification

Bench: test-samples/app_automate-wdio_mocha-android/examples/repro-triple/ — three tests with reloadSession() inside the first, and phase boundaries annotated into the session log via browserstack_executor so each scan is attributed by position rather than inferred from its command type. Verdicts read from the App Automate session-logs API.

build pre-reload session post-reload session
baseline 9.35.0 — va0eji8ln9bke4crc9jq4q0n44ru25autnebse7l 6 scans 0
this branch — jy8hf6xg3bcpw8zxd3dnsaql0zwqqis7ipvi8u5k 6 scans 16

Identical commands either side of the reload; the post-reload session goes from nothing to fully scanned, and the pre-reload half is unchanged.

Per-phase, post-fix, in the reloaded session: test 1 body RESUMED 6 scans (click ×3, back ×3) where the baseline had 0, then before each #2 1, test 2 3, before each #3 1, test 3 2 — the later tests were already recovering before this fix, which is what bounds the blast radius above.

Re-verified on the stacked head — r89xojas5cvljamjpwrj8x7zcf3pc3umt8qyoptk (sessions 72d8f60ebfd6485d670021fb68f43c4163ee6897 pre-reload, e60022566902b3f72651c4784448e2d39ff73302 post-reload), run from #168 which stacks on this branch: still 6 pre-reload and 16 post-reload, so the work layered on top does not regress the migration. That run's own subject — global-hook scans no longer carrying a test run uuid, mocha and cucumber, pre-fix and post-fix — is evidenced on #168 rather than duplicated here.

Which path owns the migration is decided by one predicate, _isCliAccessibilityFlow()isBrowserstackSession(browser) && isRunning() — used both here and where AccessibilityHandler.before() is gated, so the two cannot drift. isRunning() alone is not equivalent: with the binary up against a non-BrowserStack provider the classic handler did initialise and still holds the live state.

11 unit tests across the three files, covering the migration itself, a user-closed gate surviving it, the post-reload toggle on both paths, the module-registry lookup in the CLI flow, the non-BrowserStack-provider case, and no-op/missing ids. Full suite at parity — 75 pre-existing failures before and after, verified by stashing and re-running. npm run lint clean.

Split out of this PR

Two other things came out of the same investigation and are parked on their own branches, each needing review on its own terms rather than as a rider here:

  • SDK-7422-config-level-before-hook — driver commands in a WDIO config-level before() are never scanned. Fix is device-verified (hook window 0 → 7 scans) but needs a hook-lifecycle design, including how such a scan gets a hook_run_uuid TRA can resolve.
  • SDK-7422-hook-source-logging — logging the user's hook sources for triage, with credential redaction. Anything that writes customer code into an uploaded log needs its own review.

Earlier revisions of this branch also followed tsconfig paths aliases in config capture; that is reverted and configCapture.ts is identical to main. The commit history shows both add-then-revert cycles.

Related Jira task/s

https://browserstack.atlassian.net/browse/SDK-7422

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)

  • Fixed accessibility scanning stopping for the remainder of a test after browser.reloadSession().

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

  • accessibilityModule.onSessionReload + service.onReload: migrate the session-keyed scan gate (and LOG_DISABLED_SHOWN) onto the new session id after reloadSession(), which previously orphaned it for the remainder of the reloading test.
  • accessibility-handler: same migration for _a11yScanSessionMap and _sessionId on the classic (non-CLI) path, where _sessionId was captured once in before() and never updated. Its scanning toggles write this._sessionId — what commandWrapper reads — instead of the id captured in before().
  • accessibilityModule: scanning toggles and getAccessibilityResults/Summary resolve the session at call time (currentSessionId()); the getters previously reported the pre-reload session's results while the classic path reported the live one.
  • service: one _isCliAccessibilityFlow() predicate gates both AccessibilityHandler.before() and the reload migration.
  • 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 are gone.
  • Everything else from the original scope is split out: config-level before() scanning → SDK-7422-config-level-before-hook; hook-source logging → SDK-7422-hook-source-logging; tsconfig-alias config capture → reverted.

Checklist

  • Ready to review
  • Has it been tested locally?

PR Validations

Run Tests: Comment RUN_TESTS to trigger sanity tests.

…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
@kamal-kaur04
kamal-kaur04 requested a review from a team as a code owner August 26, 2026 08:34
Comment thread packages/browserstack-service/src/configCapture.ts Fixed
…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
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Phase-annotated pre/post verification

The bench now annotates every phase boundary into the session log with browserstack_executor annotate (PHASE_MARKER …), so each scan is attributed to a phase by position in the log rather than inferred from its command type. The spec also carries three it blocks with the reload inside the first, which splits a question that was previously conflated: does the reloading test recover, and do later tests recover?

The markers are themselves driver commands, but they carry a browserstack_executor payload, which shouldPatchExecuteScript excludes from scanning — so they add no scans of their own and counts stay comparable with the unannotated runs above.

Pre-fix — @wdio/browserstack-service@9.35.0 from npm

Build va0eji8ln9bke4crc9jq4q0n44ru25autnebse7l

Original session 59a9f5043baaf064ad9a16184d7f807b7360feb6:

phase commands issued scans
config-level before() startActivity ×2, click ×3 0 — window dark
mocha before all back ×2 2
mocha before each #1 back ×1 1
test 1, pre-reload back ×3 3
6 total

Reloaded session f5d3de3bbd66cbd804c721e6c35276afb916ae70:

phase commands issued scans
test 1, post-reload click ×3, back ×3 0 — window dark
mocha before each #2 back ×1 1
test 2 click ×1, back ×2 3
mocha before each #3 back ×1 1
test 3 back ×2 (click element absent) 2
9 total (+2 end-of-test)

Post-fix — this branch

Build xjfwleeeyemcwi7jrsbttqsetrha9wfvbiuvneub

Original session f095f87f968ee6f502db77a8ef8e485d9ee3b441:

phase scans change
config-level before() 7execute ×2, executeScript ×2, click ×3 0 → 7
mocha before all 2 unchanged
mocha before each #1 1 unchanged
test 1, pre-reload 3 unchanged
13 total from 6

Reloaded session 4c2cd4a8d74c3e397ed177b5e6fa5cd98b8ec32e:

phase scans change
test 1, post-reload 6click ×3, back ×3 0 → 6
mocha before each #2 1 unchanged
test 2 3 unchanged
mocha before each #3 1 unchanged
test 3 2 unchanged
16 total from 9

Command counts are identical across the two runs (T3_click_fail in both — the element genuinely wasn't there), so every delta above is the fix and nothing else.

Three things the annotations settle

  1. Framework hooks were never the problem. before all and before each scan identically before and after the fix — 2 and 1, in both runs, in both sessions. Only the config-level hook was dark. This retires a hypothesis that survived several rounds of this investigation on the strength of hook annotations appearing near unscanned commands.

  2. Pre-fix reload damage is bounded to the reloading test, not the session. before each chore: update package and lock files devDependencies #2 recovers on its own, because the next onBeforeTest / onHookStart registers the gate under the new session id that onReload had already published. So the cost is the remainder of the one test that called reloadSession() — 6 commands here — after which scanning resumes unaided. Worth stating precisely: earlier framing in this PR implied a wider blast radius than the log supports.

  3. The 6 ms race is only reachable by a hook whose first act is a scannable command. Both startActivity calls scanned in this run (execute ×2 + executeScript ×2), where the earlier unannotated run caught only the late one. The difference is the annotate sitting ahead of them: it is itself a device round-trip, and it absorbs the window. Any prior driver call does the same, which bounds the residual further than the previous comment claimed.

Also in this push: the CodeQL comment on the alias wildcard is addressed in e943a78.

@kamal-kaur04
kamal-kaur04 requested review from 07souravkunda and removed request for pri-gadhiya August 26, 2026 09:25
Comment thread packages/browserstack-service/src/service.ts Outdated
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
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Pre-test scans now carry a hook run TRA can resolve — f2271ca

Follow-up to the config-level before() fix. Stamping a uuid on the scan payload was not enough: App-A11y resolves a scan's parent by looking the hook_run_uuid up in BTCER, so a uuid with no hook event behind it is no better than none. Evidence from the previous build — the pre-test scans went out with both identifiers missing:

PHASE >> config-level before() STARTED
   scan  method=execute        testRun=ABSENT  hookRun=ABSENT      ← belongs to nothing
PHASE >> mocha before all STARTED
   scan  method=back           testRun=8e466b38  hookRun=6caa3175

What now happens

The config-level window is reported as a BEFORE_ALL hook run through the existing framework path — framework.trackEvent(BEFORE_ALL, PRE/POST). That means the framework mints the uuid (wdioMochaTestFramework at hook PRE), the binary turns the event into HookRunStarted/HookRunFinished (HOOK_REGEXonHookRunStartedmakeHook), and this module's own onHookStart observer stamps the same uuid onto the scans. No new plumbing, one source of truth for the uuid.

Two things fell out better than designed:

  • resolveInstance already creates an instance for a BEFORE_* PRE when none exists (it has to — suite-level before all fires before any INIT_TEST), so no special-casing was needed.
  • That instance is the one the first test then reuses, so the hook lands with test_run_id populated with the real first-test uuid rather than orphaned. We agreed the uuid alone would do; it turns out we get the parent too.

Verified on build axzkajmr2odsx8dvyx2wfxw6mxtec3d9sbpmp08t:

HookRunStarted   BEFORE_ALL   176528f5  wdio config-level before hook (pre-test window)
HookRunFinished  BEFORE_ALL   176528f5  wdio config-level before hook (pre-test window)
HookRunStarted   BEFORE_ALL   11517014  "before all" hook: wrappedHook for "test 1 …"
HookRunStarted   BEFORE_EACH  49ec63a1  "before each" hook: wrappedHook for "test 1 …"

and every scan in that window now resolves to it:

PHASE >> config-level before() STARTED
   scan  hookRun=176528f5  testRun=d5b41d78     ×7
PHASE >> mocha before all STARTED
   scan  hookRun=11517014  testRun=d5b41d78     ×2
PHASE >> mocha before each #1 STARTED
   scan  hookRun=49ec63a1  testRun=d5b41d78
PHASE >> test 1 body STARTED
   scan  hookRun=ABSENT    testRun=d5b41d78     ×3   ← test-body scans still un-hooked, as they should be

It only fires when a user actually has that hook

Opening this from the WDIO lifecycle would have reported a hook that never ran on every build in the fleet — the overwhelming majority of suites have no config-level before(). And it cannot be detected from config: ConfigParser.addService folds the user's config hooks and every service's hooks into one config.before array (each hook.bind(service), so even name inspection fails), and the runner fires them together with Promise.all. A service is a sibling of the user's hook, not an observer of it.

So the hook run is opened on demand from the scan path — the exact condition that matters, since the only reason it exists is to parent a scan. A/B on the same spec, one config with the hook and one without:

config hook events reported
with config-level before() 5 hook runs — 4 Mocha + 1 pre-test window
without it — lpbvftwaslksugogditfsvc1pspzd4fwgtneyxnh 4 hook runs — Mocha only, nothing extra

Bench control kept at examples/repro-triple/repro-nohook.conf.ts.

Notes for review

  • 'idle' → 'attempted' → 'open' → 'closed' rather than a boolean: attempted is what stops a failed PRE from being retried on every wrapped command and from being closed by a POST that would pair with nothing.
  • The state is set before the await, so concurrent commands cannot each open their own hook run.
  • The window is closed at the first onBeforeTest. Its duration therefore includes any Mocha hooks that ran in between — the end of a user's config hook is genuinely not observable, so this is an upper bound, and the scans inside those Mocha hooks still carry their own hook uuids.
  • 4 unit tests: opens once and only once, never opens while a framework hook is already the parent, closes at the first test, never closes what it did not open.
  • Full suite at parity (75 pre-existing failures before and after); lint clean.

…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
@kamal-kaur04 kamal-kaur04 changed the title fix(app-a11y): scan the pre-test and reloaded-session windows, capture alias-imported configs fix(app-a11y): keep scanning across reloadSession, and log the user's hook sources Aug 26, 2026
Comment thread packages/browserstack-service/src/hookSources.ts Fixed
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Scope narrowed — ef9953a

Two things came out of this PR, so the diff is smaller than the discussion above it. Reviewers reading from the top: the earlier comments describe work that is no longer in this branch.

Split outSDK-7422-config-level-before-hook (pushed, no PR yet). The config-level before() fix and everything built on it: the driver-creation scan gate, the on-demand BEFORE_ALL hook run for the pre-test window, and the hook-window instrumentation. It works and is verified on device — the ledger of that is in this PR's history — but it needs a hook-lifecycle design that is not a day's work, and it should not hold up the reload fix.

Reverted → the tsconfig paths alias following in config capture (e943a78 and the alias part of d2a0326). It widened what gets uploaded out of a customer's project, and the thing we actually needed — the hook code behind a failure — does not require uploading anything.

Replaced with → reading the hook bodies out of the config file and logging them, plus publishing which hooks call reloadSession. The parsed config is no help here: ConfigParser binds every hook it folds in, and a bound function stringifies to [native code].

So this branch is now exactly: the reloadSession scan-gate migration (both the CLI and classic paths) and hook-source logging. configCapture.ts is untouched relative to main.

One note carried over for whoever picks up the split branch: the CodeQL finding that was fixed in e943a78 lived in the alias-resolution code, which no longer exists here — the alert is moot on this branch rather than fixed. If alias resolution ever returns for extraction, the wildcard substitution needs the same slicing treatment (String.replace interprets $& in the replacement, which corrupted the resolved path).

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
@kamal-kaur04 kamal-kaur04 changed the title fix(app-a11y): keep scanning across reloadSession, and log the user's hook sources fix(app-a11y): keep scanning across browser.reloadSession() Aug 26, 2026
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Scope final — reloadSession only (71b3966)

The hook-source logging is out of this PR too. Anything that writes a customer's own code into an uploaded log deserves its own review, and it should not ride along on a scan-gate fix.

This branch is now 125 lines across three source files, and configCapture.ts, constants.ts and launcher.ts are identical to main.

Answering the credential question before it moves anywhere

The hook-source work went to SDK-7422-hook-source-logging with the redaction fixed, because as first written it was a leak. BStackLogger.redactCredentials is applied to every log line, but it only knows user/key/userName/accessKey in a key-value or query-string position. Measured against a hook body carrying nine sensitive shapes, six survived it:

shape logger scrub redactSensitiveContent
accessKey, apiKey, userName redacted redacted
authToken, password, clientSecret, inline token: readable redacted
AWS_SECRET_ACCESS_KEY (snake_case) readable redacted
URL userinfo https://admin:…@host readable redacted
PEM private-key block readable redacted

So captured sources now go through redactSensitiveContent — the same routine that already guards an uploaded config file — rather than relying on the logger.

One ordering detail that matters there: the identifier scan had to move ahead of redaction. 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 vanish from the very detection it feeds. Raw text is scanned first, then the stored copy is redacted.

Also: the test fixtures use neutral sentinels rather than realistic tokens. The repo's gitleaks guard rejected the commit when they looked like real secrets, and it was right to — the redactor keys on the field name, not the value shape, so nothing is lost.

Where the rest went

branch what why it's not here
SDK-7422-config-level-before-hook config-level before() commands unscanned; the BEFORE_ALL hook run for that window; hook-window instrumentation needs a hook-lifecycle design
SDK-7422-hook-source-logging hook sources logged, redacted writes customer code into an uploaded log
tsconfig-alias following in config capture reverted; widened what gets uploaded

All device-verified; the evidence for each is in this PR's comment history so it survives the merge.

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
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Re-verified E2E after the review fix — 5da67af

Fresh pre/post pair on the reload-only branch, phase-attributed from each session's own log. Same bench, same spec, three tests with reloadSession() inside the first.

Pre-fix@wdio/browserstack-service@9.35.0 from npm — tizqgwo6rej7rujzdtkvqks6gj0has8ptgnt9yek

phase original session 48e4d25f… reloaded session 10d05433…
config-level before() 0
mocha before all 2
mocha before each #1 1
test 1, pre-reload 3
test 1, post-reload 0
before each #2 1
test 2 3
before each #3 1
test 3 2
6 9 (incl. 2 end-of-test)

Post-fix — this branch — pdneyhvmmyowe85n7jzcxmi23nyru15xukh9flju

phase original session fcfbe107… reloaded session 8980fd6f…
config-level before() 0
mocha before all 2
mocha before each #1 1
test 1, pre-reload 3
test 1, post-reload 6click ×3, back ×3
before each #2 1
test 2 3
before each #3 1
test 3 2
6 16 (incl. 3 end-of-test)

Exactly one phase moves: 0 → 6, the remainder of the test that called reloadSession(). Every other row is identical across the two runs, including the pre-reload session totalling 6 in both — which is the check that the fix is narrow rather than generally louder.

Two things this pair also confirms:

  • The later tests were already recovering without the fixbefore each #2 onward is 9 scans pre-fix. That is the evidence behind the bounded-blast-radius claim in the description: a reload costs the remainder of one test, not the session.
  • The config-level before() window is still dark in both, 0 scans either side. Correct for this PR — that fix is on SDK-7422-config-level-before-hook, and its absence here is visible rather than assumed.

Unit side after the review fix: 82 passing across the two accessibility suites, service.test.ts back at its 40-failure baseline with the new flow-gating test passing (the +1 failure my first cut introduced was the new test itself hitting _printSessionURL's live fetch — now stubbed, since that is not what the test is about). Full suite 75 pre-existing failures, unchanged. Lint clean.

@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

RUN_TESTS

Comment thread packages/browserstack-service/src/cli/modules/accessibilityModule.ts Outdated
Comment thread packages/browserstack-service/src/cli/modules/accessibilityModule.ts Outdated
Comment thread packages/browserstack-service/src/service.ts
Comment thread packages/browserstack-service/src/cli/modules/accessibilityModule.ts Outdated
Comment thread packages/browserstack-service/tests/service.test.ts Outdated
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
@kamal-kaur04
kamal-kaur04 requested review from rounak610 and removed request for dandonarahul2002 August 26, 2026 13:41
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

RUN_TESTS

@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

rounak610
rounak610 previously approved these changes Aug 26, 2026
shivam5643
shivam5643 previously approved these changes Aug 26, 2026
Comment thread packages/browserstack-service/src/service.ts
@shivam5643

Copy link
Copy Markdown
Collaborator

PR Review: wdio-browserstack-service PR #165

Summary

Intent: Migrate the accessibility scan-gate state (and, for the classic handler, _sessionId itself) onto the new session id inside browser.reloadSession(), and resolve the session id at call time in the scanning toggles / results getters, so accessibility scanning no longer silently stops for the remainder of a test after a mid-test reload.
Risk: Medium
1 critical · 0 warnings · 0 suggestions | Files reviewed: 7

═══════════════════════════════════════════════════════════════

Findings

Two channels. Blocking = Critical + Warning — the must-fix set the Verdict gates on. Non-blocking = Suggestions — polish; best-effort, never gates.

🔴 Critical (Blocking)

# Finding File · Symbol Confidence
1 [Correctness] New private method inserted between an existing decorator and its target silently re-targets the decorator packages/browserstack-service/src/service.ts · _isCliAccessibilityFlow / onReload 🟢

1. [Correctness] New private method inserted between an existing decorator and its target silently re-targets the decoratorpackages/browserstack-service/src/service.ts · _isCliAccessibilityFlow / onReload

Problem:
@PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'onReload' }) was already on onReload before this PR. This PR adds the new _isCliAccessibilityFlow() private method directly between that decorator and async onReload(...), with only a JSDoc comment (which the parser treats as trivia, not a declaration) in between. A class-member decorator applies to the immediately following member declaration, so after this change the decorator wraps _isCliAccessibilityFlow and onReload is left completely undecorated. Verified against the full HEAD file (cec234c): the decorator sits at line 888, _isCliAccessibilityFlow at line 899, and onReload — now undecorated — at line 903. On the base commit (2ec87f8) the decorator sat at line 887 directly above async onReload at line 888, so this is a regression introduced by this PR.

Two effects, both silent (no compile error, no test failure — the new unit tests call service.onReload(...) directly and assert on mock call args, which the decorator's presence or absence does not change):

  1. onReload — the WDIO reload hook this whole PR is about — loses its PerformanceTester.Measure instrumentation. Any perf/SDK-hook telemetry that previously fired with hookType: 'onReload' on every browser.reloadSession() stops firing.
  2. _isCliAccessibilityFlow() — a small synchronous boolean helper called from at least two other call sites in this same file (the before() hook gate, and again inside onReload's own migration guard) — now gets wrapped and measured under the onReload hook-type label on every call, including calls made from the before() hook. That pollutes SDK-hook performance telemetry with mismatched hook-type labels for a method that isn't a hook at all.

Neither effect throws or fails a test, which is exactly why it is easy to miss — but it is a real, unintended change in what gets measured and correlated in SDK performance/observability data.

Suggested Fix:
Move _isCliAccessibilityFlow() so it is not between the decorator and onReload — e.g. declare the private helper method above the decorator (or anywhere else outside the decorator/target pair), so @PerformanceTester.Measure(..., { hookType: 'onReload' }) sits immediately above async onReload(...) again with nothing but its own JSDoc (if any) in between.

Confidence: 🟢 — objectively verifiable from TypeScript/JS class-member decorator binding semantics and confirmed against the full file at both base and HEAD.

───────────────────────────────────────────────────────────────

🟠 Warnings (Blocking)

None.

💡 Suggestions (Non-blocking)

None.

═══════════════════════════════════════════════════════════════

External Services

No external-contract changes detected.

═══════════════════════════════════════════════════════════════

Per-File Confidence (for reviewers)

File Status Reason
.changeset/pr-165.md ✅ All Clear Changelog entry accurately describes the fix, no contradiction with the code
packages/browserstack-service/src/accessibility-handler.ts ✅ All Clear Reload migration + call-time session resolution reviewed, no issues found
packages/browserstack-service/src/cli/modules/accessibilityModule.ts ✅ All Clear Reload migration, currentSessionId(), and the Map<string, boolean> type fix reviewed, no issues found
packages/browserstack-service/src/service.ts 🔴 Author to Fix 1 1 objectively verifiable defect — decorator now binds to the wrong method
packages/browserstack-service/tests/accessibility-handler.test.ts ✅ All Clear New onSessionReload tests carry real assertions, no issues found
packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts ✅ All Clear New onSessionReload + post-reload toggle tests carry real assertions, no issues found
packages/browserstack-service/tests/service.test.ts ✅ All Clear New accessibility-state-migration tests reviewed, no issues found

═══════════════════════════════════════════════════════════════

What's Good

  • _isCliAccessibilityFlow() unifies the predicate gating both AccessibilityHandler.before() and the reload migration branch, directly closing the gap a reviewer (07souravkunda) flagged inline on this PR — the non-BrowserStack-provider case is now covered by a dedicated test.
  • Both onSessionReload implementations (classic handler + CLI module) migrate the gate entry rather than re-deriving it, so a stopA11yScanning() called before the reload survives the migration — verified by a dedicated test in each file.
  • 11 new unit tests cover the migration itself, a user-closed gate surviving it, both toggle paths, the no-op/missing-id cases, and the CLI-module lookup path.

═══════════════════════════════════════════════════════════════

Coverage Ledger

Unit Files Regions Judged Issues
u001 (low risk) .changeset/pr-165.md 1 1 0
u002 (medium risk) accessibility-handler.ts, accessibilityModule.ts + their tests 14 14 0
u003 (medium risk) service.ts + service.test.ts 6 6 1
cross-cutting (whole-PR pass) all files ran, 0 findings 0

Totals: 21/21 regions judged, coverage_gap = 0. No high-risk units, so no second pass was triggered. Binary-pairing gate: no .proto/generated-stub files touched → SDK-only, no paired Binary PR needed.

═══════════════════════════════════════════════════════════════

Verdict

🔴 Fix 1 blocking issue

service.ts: the new _isCliAccessibilityFlow() private method is inserted between the pre-existing @PerformanceTester.Measure(..., { hookType: 'onReload' }) decorator and onReload, silently re-targeting the decorator onto the helper and leaving onReload itself undecorated.

═══════════════════════════════════════════════════════════════

— SDK PR Review Agent

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
@kamal-kaur04
kamal-kaur04 dismissed stale reviews from shivam5643 and rounak610 via bd75537 August 26, 2026 14:33
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

RUN_TESTS

Comment thread packages/browserstack-service/tests/decoratorPlacement.test.ts
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

@shivam5643

Copy link
Copy Markdown
Collaborator

SDK PR Review — ✅ Good to go

Head reviewed: bd75537965a2f99bdd31cde161abf569decef33f
Mode: incremental (prior verdict at cec234c198b3ce6fa60f8b6a3f1864185a1c1184)

Prior blocking finding — RESOLVED

packages/browserstack-service/src/service.ts — the @PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'onReload' }) decorator is now positioned directly above async onReload(oldSessionId: string, newSessionId: string), with the _isCliAccessibilityFlow() helper (and its JSDoc) moved entirely above the decorator. The decorator binds to onReload again — confirmed by reading the class element sequence at the new head:

    private _isCliAccessibilityFlow (): boolean {
        return Boolean(isBrowserstackSession(this._browser)) && BrowserstackCLI.getInstance().isRunning()
    }

    @PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'onReload' })
    async onReload(oldSessionId: string, newSessionId: string) {

Nothing but a blank line stands between the decorator and its target.

New finding (non-blocking) — the new guard's file list misses 3 real decorator sites

packages/browserstack-service/tests/decoratorPlacement.test.tsDECORATED_FILES = ['service.ts', 'launcher.ts', 'insights-handler.ts', 'accessibility-handler.ts'] is both over- and under-inclusive relative to actual @PerformanceTester.Measure usage in src/. Counted across every .ts file in src/ at bd75537:

File @PerformanceTester.Measure sites In DECORATED_FILES?
service.ts 15
launcher.ts 4
Percy/Percy.ts 2 ❌ missed
Percy/PercyBinary.ts 1 ❌ missed
insights-handler.ts 0 listed, nothing to check
accessibility-handler.ts 0 listed, nothing to check

insights-handler.ts and accessibility-handler.ts use only .start()/.end()/.measureWrapper(), so their it() blocks run against zero decorator lines and trivially pass — harmless, but the list doesn't mean what its name implies. Meanwhile Percy/Percy.ts (PERCY_EVENTS.START, PERCY_EVENTS.STOP) and Percy/PercyBinary.ts (PERCY_EVENTS.DOWNLOAD) carry the exact fragility this test was written to close off, and none of those 3 sites are currently checked. This makes the docstring's "Nothing else in this repo can catch that" overstate the guard's reach.

Recommend adding 'Percy/Percy.ts' and 'Percy/PercyBinary.ts' to DECORATED_FILES in a follow-up (the existing path.join(process.cwd(), 'src', file) already reaches subdirectories).

A second, narrower gap in the same check: it flags only when the next non-blank line starts with /*, *, or //. That catches the exact shape that caused this regression (decorator → JSDoc → real method), but a method spliced in without a leading comment would present a non-comment line and pass silently. Worth tightening if the check is meant to cover the whole bug class rather than the instance that motivated it.

Neither point blocks this PR — they don't touch the fix and don't reintroduce the bug being fixed.

Scope note

Per commit-graph inspection (GET /compare/cec234c...bd75537ahead_by: 1, total_commits: 1), the only commit added since the prior verdict is bd75537 itself — src/service.ts +13/−7 and tests/decoratorPlacement.test.ts new +41. 5da67af and 4803e0f are ancestors of cec234c (chain: 4803e0f → 5da67af → cec234c → bd75537) and were already inside the snapshot the prior review judged. 5da67af's content — dropping the this._sessionId === null adopt-any-reload fallback in accessibility-handler.ts, and splitting the classic-handler migration into the CLI-flow/else guard in service.ts — was re-read against the current head for grounding and is correct and internally consistent with _isCliAccessibilityFlow()'s gating of AccessibilityHandler.before(). No new finding there.

Coverage ledger

Region Judged Notes
src/service.ts (decorator relocation + docstring correction, ~lines 895-911) Verified against fetched file content at bd75537
tests/decoratorPlacement.test.ts (new file, 41 lines) Logic verified; check's algorithm run over all listed files at bd75537; DECORATED_FILES coverage gap found

Regions judged: 2/2 (100%), coverage_gap = 0. No .proto/generated-stub files touched → SDK-only, no paired Binary PR needed.

Falsified / dropped findings

None dropped. One candidate concern was investigated and closed as not-a-bug: whether accessibilityMap's Map<number, boolean>Map<string, boolean> retype could produce a TS error against AutomationFramework.getState(). TrackedInstance#getAllData() returns Map<string, any> and getState() returns any, so the retype is a straightforward correctness fix (session ids are strings), not a defect.

Verdict

No blocking issues. The prior blocking finding is resolved and verified against real code at the new head. One non-blocking follow-up noted on the new guard's file coverage.

— SDK PR Review Agent

@hamza-browserstack
hamza-browserstack merged commit 23d45bb into main Aug 27, 2026
16 of 18 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.

6 participants