-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
.pr_agent_accepted_suggestions
| PR 17795 (2026-07-17) |
[performance] Overbroad upload glob
Overbroad upload glob
In `read-targets`, the `*-targets.txt` upload glob also matches the downloaded `bazel-targets.txt`, so the large aggregate affected-target list gets re-uploaded inside the `targets` artifact whenever that file is present. This unnecessarily increases artifact size/transfer and duplicates content already stored in the `check-targets` artifact.The targets artifact upload uses a broad glob (*-targets.txt) that also matches bazel-targets.txt (downloaded from the check-targets artifact). This causes the large aggregate target list to be included in the targets artifact unintentionally.
Downstream Python/Ruby jobs only need py-targets.txt / rb-targets.txt. The aggregate bazel-targets.txt is already available via the check-targets artifact and doesn’t need to be duplicated.
- .github/workflows/ci.yml[74-111]
Replace path: "*-targets.txt" with an explicit allowlist, e.g.
path: |
py-targets.txt
rb-targets.txt(or upload from a dedicated directory that only contains per-binding files).
| PR 17789 (2026-07-16) |
[correctness] Missing runtime resource copies
Missing runtime resource copies
py:local_dev only copies a directory allowlist, so generated single-file outputs (e.g. common/mutation-listener.js, common/bidi-mutation-listener.js, firefox/webdriver_prefs.json) are not staged into py/selenium/webdriver in the default mode. Python code loads these resources at runtime and will raise (ValueError/FileNotFoundError) when they’re missing.py:local_dev only iterates over a list of directories and copies **/* beneath them. Several Bazel-generated resources are files that live outside those directories (e.g., common/mutation-listener.js, common/bidi-mutation-listener.js, firefox/webdriver_prefs.json), so they never get staged in the default ./go py:local_dev path, causing runtime failures when Selenium tries to load them.
- These resources are produced by
copy_file(...)outputs inpy/BUILD.bazel. - Runtime Python code expects them to exist in the source tree when running from source (the purpose of
py:local_dev).
- rake_tasks/python.rake[68-94]
- Extend the staging logic to handle both directories and individual files.
- Add explicit handling (in the default mode) to copy:
common/mutation-listener.jscommon/bidi-mutation-listener.jsfirefox/webdriver_prefs.json
- Optionally, in
allmode, detect whetherDir.children(bazel_bin)entries are files vs directories and copy root files correctly (copy the file itself, not only**/*).
[correctness] Any arg triggers full copy
Any arg triggers full copy
The task now treats any non-nil value for the :all argument as enabling the “copy everything” mode, so `./go py:local_dev foo` behaves like `all` despite the help text. This makes the task’s behavior unpredictable and can cause much broader deletion/copying than intended.dirs = arguments[:all] ? ... treats any provided argument value as truthy, not just the documented all flag, so arbitrary arguments unexpectedly enable the full-copy path.
The ./go wrapper forwards any extra CLI args into the rake task’s bracket args (task[args]). With the current truthiness check, any arg value will switch behavior.
- rake_tasks/python.rake[75-75]
- Change the condition to
arguments[:all] == 'all'(or explicitly validate accepted values and abort on unknown values).
- Change the condition to
- go[21-40]
- (No change required, but this shows why arbitrary args are easy to pass.)
[reliability] Untracked files may be deleted
Untracked files may be deleted
The safety check only inspects diffs vs HEAD before `rm_rf`, so untracked files under the destination directories can be removed without triggering the abort. After deletion, the checkout restores tracked files from HEAD but cannot restore deleted untracked files.Before deleting dest_dir, the task only checks git.diff('HEAD') scoped to dest_dir. Untracked files are not part of HEAD, so they can be deleted by FileUtils.rm_rf(dest_dir) without warning.
This task is intentionally destructive (it clears and repopulates directories). The current guard protects modified tracked files but can still lose untracked work (scratch files, local artifacts, etc.) inside these directories.
- rake_tasks/python.rake[77-93]
- Add a guard that also detects untracked files under
dest_dir(e.g., viaSeleniumRake.git.statusif available, or by invokinggit status --porcelain --untracked-files=all -- <path>and checking for output). - Abort with a clear message listing the offending paths.
- Add a guard that also detects untracked files under
[quality] `Restore` comment restates code
`Restore` comment restates code
The newly added comment `# Restore any git tracked files in the directories` restates what the immediately following line does without adding rationale or constraints. This reduces signal-to-noise in comments and conflicts with the project guideline to prefer rationale-focused comments.A newly added comment restates the code behavior without explaining why the behavior is needed.
The comment sits directly above a checkout_file call whose intent is already clear from the method name and arguments.
- rake_tasks/python.rake[92-93]
| PR 17781 (2026-07-15) |
[reliability] Method toLowerCase crash
Method toLowerCase crash
In projectSchema(), the new link helper calls key.toLowerCase() unconditionally, so a malformed command/event entry with a missing/non-string method will throw a TypeError before checkSchema/checkCompleteness can produce actionable validation errors. This makes schema generation less diagnosable in failure scenarios.projectSchema() defines link = (map, key) => map?.[key.toLowerCase()], which will throw if key is not a string (e.g., malformed c.method/e.method). This crash happens before schema validation, reducing error quality.
Spec links are optional; missing methods should ideally be reported by existing schema/model validators instead of causing an unhandled exception.
- javascript/selenium-webdriver/project_bidi_schema.mjs[474-495]
- Update
linkto be defensive:const link = (map, key) => (typeof key === 'string' ? map?.[key.toLowerCase()] : undefined)
- (Optional) Add a small unit test covering a model entry with a non-string
methodto ensure the projector fails gracefully (or emits validation errors).
[reliability] Double-quote-only id parsing
Double-quote-only id parsing
extractAnchors() only matches HTML ids written as id="…", so valid HTML that uses single quotes (id='…') will be ignored, resulting in silently missing entries in anchors.json and therefore missing specHref links. This is brittle against future changes in the pinned spec HTML serialization and in the documented URL-based ad-hoc mode.extractAnchors() uses /id="([^"]+)"/g, which ignores id='...' (valid HTML). That can silently drop anchors and reduce specHref coverage.
This tool intentionally does a lightweight extraction (not a full DOM parse), but it should still handle common quoting forms in HTML output.
- javascript/selenium-webdriver/extract_bidi_anchors.mjs[58-72]
- javascript/selenium-webdriver/project_bidi_schema_test.mjs[485-513]
- Expand the regex to capture both quote styles, e.g.:
/id=(?:"([^"]+)"|'([^']+)')/g- then pick
m[1] ?? m[2].
- Add/extend a unit test to include an element like
<h2 id='module-session'>and assert it is indexed.
[reliability] No fetch status check
No fetch status check
When --spec is a URL, extract_bidi_anchors.mjs reads response.text() without checking response.ok, so 404/500 pages can still produce an anchors.json file and exit successfully. This can hide failures and yield incorrect/missing anchors in ad-hoc runs.For URL input, fetch(spec) is not checked for HTTP error statuses; response.text() will still succeed on 404/500 and the script will write output and exit 0.
This affects the documented ad-hoc mode (--spec as a URL). Bazel uses a pinned local file, but developers running it manually can get silently bad output.
- javascript/selenium-webdriver/extract_bidi_anchors.mjs[91-105]
- Replace the URL branch with:
const res = await fetch(spec)if (!res.ok) throw new Error(...)const html = await res.text()
- Consider including the status code and URL in the thrown error message for debuggability.
| PR 17775 (2026-07-13) |
[maintainability] Docs theme reference stale
Docs theme reference stale
py/docs/README.rst still claims the docs use sphinx-material, but conf.py now configures pydata_sphinx_theme. This inconsistency can mislead contributors who follow the README to understand or modify the docs toolchain.py/docs/README.rst still states that the docs use the sphinx-material theme, but the Sphinx config now uses pydata_sphinx_theme.
This PR changes the configured HTML theme in py/docs/source/conf.py, so any contributor-facing docs that describe the theme should be updated to match.
- py/docs/README.rst[41-46]
- py/docs/source/conf.py[114-143]
[maintainability] RST path markup wrong
RST path markup wrong
`py/docs/README.rst` marks the output directory with single backticks, which is interpreted-text markup rather than an inline literal, so the rendered docs will format the filesystem path inconsistently with other path literals in the same document.The output directory path is wrapped in single backticks, which is interpreted-text markup in reStructuredText and renders differently than inline code/literal formatting.
This README already uses inline-literal formatting (double backticks) for paths (e.g., py/docs), so the output path should use the same literal markup for consistent rendering.
- py/docs/README.rst[18-18]
| PR 17769 (2026-07-13) |
[quality] `SubscriptionScope.contexts()` missing Javadoc
`SubscriptionScope.contexts()` missing Javadoc
The new public API methods `contexts(...)` and `userContexts(...)` in `SubscriptionScope` lack Javadoc, which makes the new API harder to understand and violates the requirement for complete public method documentation (including tags).SubscriptionScope introduces new public methods but they have no Javadoc. This violates the project requirement to document public API methods and include complete tags.
SubscriptionScope is public and annotated @Beta, so it is part of the user-visible Java API surface and should be documented.
- java/src/org/openqa/selenium/bidi/SubscriptionScope.java[36-44]
[reliability] Scope retains mutable sets
Scope retains mutable sets
SubscriptionScope stores caller-provided Set instances and places them directly into the session.subscribe params map. If those sets are mutated concurrently with JSON serialization, JsonOutput’s Collection iteration may throw ConcurrentModificationException (or serialize inconsistent data), causing subscription setup to fail.SubscriptionScope keeps direct references to the caller’s Set<String> instances (contexts, userContexts) and returns them via toMap(). Those sets are later serialized by Connection.send() using JsonOutput, which iterates Collection values; if a caller mutates a non-thread-safe set concurrently, serialization can throw or produce inconsistent output.
This is primarily an API-ownership / thread-safety hardening issue: callers can pass mutable sets (e.g., HashSet) and reuse them across threads while subscribing.
- java/src/org/openqa/selenium/bidi/SubscriptionScope.java[33-54]
- Snapshot inputs in the setters, e.g.
this.contexts = Set.copyOf(contexts);andthis.userContexts = Set.copyOf(userContexts);. - (Optional) If you want to preserve insertion order for JSON output, consider copying to
List.copyOf(...)intoMap()instead of returning aSet.
[correctness] BiDi scope cross-binding mismatch
BiDi scope cross-binding mismatch
The new Java API adds `userContexts` scoping support, but other bindings appear to differ (some support only `contexts`), and the change is not documented as an intentional cross-binding divergence.This PR changes user-visible BiDi subscription scoping behavior in the Java binding, but there is no nearby documentation explaining how this aligns (or intentionally diverges) from other language bindings.
Other bindings implement session.subscribe with varying support for contexts and userContexts. The Java binding now exposes both via SubscriptionScope.
- java/src/org/openqa/selenium/bidi/SubscriptionScope.java[26-44]
- java/src/org/openqa/selenium/bidi/BiDi.java[120-130]
| PR 17759 (2026-07-10) |
[architecture] New `Script` methods not default
New `Script` methods not default
The new `Script` interface methods are abstract and provide no default implementation or in-code justification, forcing all implementers to update immediately.New interface methods were added to org.openqa.selenium.remote.Script without default bodies and without a design note explaining why defaults are not feasible.
Even though this API is @Beta, adding abstract methods to an existing public interface is a source/binary compatibility hazard for any external implementations.
- java/src/org/openqa/selenium/remote/Script.java[43-46]
- java/src/org/openqa/selenium/remote/Script.java[61-64]
- java/src/org/openqa/selenium/remote/Script.java[79-82]
[quality] `@Deprecated` missing `forRemoval`
`@Deprecated` missing `forRemoval`
`RemoteScript` adds several `@Deprecated` annotations without `forRemoval = true`, which violates the project deprecation policy and creates inconsistent removal intent across the API surface.RemoteScript introduces @Deprecated annotations that omit forRemoval = true, which violates the deprecation compliance rule.
This PR already uses @Deprecated(since = "4.46", forRemoval = true) in org.openqa.selenium.remote.Script, but the overriding/implementing methods in RemoteScript were annotated as plain @Deprecated.
- java/src/org/openqa/selenium/remote/RemoteScript.java[71-159]
[reliability] Legacy id lost on failure
Legacy id lost on failure
RemoteScript.resolveLegacyId removes the legacy-id mapping before attempting to unsubscribe; if unsubscribe fails, the legacy id is permanently lost even though the underlying subscription may still be active. This can strand subscriptions and prevent retrying cleanup via the deprecated long-based API.The deprecated long-based removal flow deletes the legacy-id mapping before the actual unsubscribe happens, so failures leave no way to retry removal.
resolveLegacyId uses legacySubscriptionIds.remove(id) and is called before invoking biDi.removeListener(subscriptionId).
- java/src/org/openqa/selenium/remote/RemoteScript.java[77-91]
- java/src/org/openqa/selenium/remote/RemoteScript.java[166-178]
Change the flow to:
- look up subscription id without removing it (e.g.,
get), - attempt
biDi.removeListener(subscriptionId), - only upon success, remove the legacy mapping. If unsubscribe throws, keep the mapping so users can retry cleanup.
[reliability] Unchecked subscription id
Unchecked subscription id
BiDi.subscribe casts result.get("subscription") to String without validating presence/type, so a malformed or unexpected response can produce a null/invalid subscriptionId that later fails in listener registration with a less actionable error. Adding validation would improve robustness and diagnostics for protocol mismatches.subscribe(...) returns (String) result.get("subscription") without checking for null/type/blank.
Downstream code requires a non-null subscription id; if the response is missing/invalid, the exception will be thrown later and may not clearly explain the real protocol problem.
- java/src/org/openqa/selenium/bidi/BiDi.java[122-125]
- java/src/org/openqa/selenium/bidi/Connection.java[178-187]
Extract the value, validate it is a non-blank String, and if not, throw a WebDriverException/BiDiException including the returned result map (or at least the keys) to aid debugging.
| PR 17758 (2026-07-09) |
[reliability] Unsubscribe leaks remote subscription
Unsubscribe leaks remote subscription
`Module.unsubscribe` delegates to `Handle.unsubscribe`, which only removes the local callback (`BiDi.removeListener`) and never issues `session.unsubscribe`, so the browser can keep streaming events even after callers “unsubscribe”. Because `Connection.isEventSubscribed` is derived from the local callback map, once the last listener is removed the later `BiDi.clearListener(event)` path will not send `session.unsubscribe`, making the remote subscription effectively impossible to clean up for that event within the same connection.Handle.unsubscribe(long) maps to BiDi.removeListener(long) which only mutates local listener state; it does not send session.unsubscribe. This creates a mismatch with subscribe, which does send session.subscribe, leading to leaked remote subscriptions and unnecessary event traffic.
-
BiDi.addListenersendssession.subscribeon every subscription. -
BiDi.removeListeneronly removes local callbacks. -
BiDi.clearListener(event)only sendssession.unsubscribewhenConnection.isEventSubscribed(event)is true, butConnection.removeListenerremoves the event entry when the last local listener is removed.
- java/src/org/openqa/selenium/bidi/Handle.java[38-48]
- java/src/org/openqa/selenium/bidi/Module.java[44-54]
- java/src/org/openqa/selenium/bidi/BiDi.java[89-167]
- java/src/org/openqa/selenium/bidi/Connection.java[205-254]
- Introduce a subscription type that retains enough info to unsubscribe on the wire (e.g.,
SubscriptionholdingEvent, optional context IDs, and local listener id), and exposeunsubscribe(Subscription)/Subscription.close(). - Alternatively, teach
Connection.removeListener/BiDi.removeListenerto perform asession.unsubscribewhen the last local handler for an event is removed (requires tracking whether the subscription was global vs context-scoped, leveragingcontextListenerIdsinBiDi).
[maintainability] Handle lacks @Beta annotation
Handle lacks @Beta annotation
Handle is introduced as a public API type and is exposed via HasBiDi#getHandle(), but unlike the surrounding BiDi surface it is not annotated @Beta, which can mis-signal it as a stable/committed API despite being intended as an internal-ish transport handle.org.openqa.selenium.bidi.Handle is a newly added public type and is returned from the public HasBiDi#getHandle() API, but it is not annotated with @Beta even though the rest of the BiDi-facing surface in this area is.
This is primarily an API-signaling/maintenance problem: it can encourage downstream code to treat Handle as stable even if Selenium intends to keep it flexible while generated BiDi modules are evolving.
HasBiDi is annotated @Beta and now exposes Handle directly. Module and BiDi are also @Beta, but Handle itself is not.
- java/src/org/openqa/selenium/bidi/Handle.java[18-31]
Add import org.openqa.selenium.Beta; and annotate the class with @Beta (or otherwise make the intended stability explicit).
| PR 17756 (2026-07-07) |
[maintainability] Relative output_base footgun
Relative output_base footgun
`AGENTS.md` recommends `--output_base=.local/bazel-out`, but because it’s a relative path it can resolve under whatever directory Bazel is invoked from, potentially creating build output outside the repo-root `.local/` tree. This can lead to unignored generated directories (e.g., `java/.local/...`) since the repo `.gitignore` only ignores the top-level `/.local/*`.AGENTS.md documents --output_base=.local/bazel-out as a workaround, but this is a relative path and may land outside the repo-root .local/ (and therefore outside the only .local ignore rule) if Bazel is run from a subdirectory.
The repo .gitignore ignores only the top-level /.local/*, not nested .local directories elsewhere. Some repo docs also explicitly instruct running Bazel from the repo top-level directory.
- AGENTS.md[25-30]
Adjust the guidance to ensure the output base is anchored to the repo root, e.g.:
- Explicitly instruct running Bazel from the repo root before using
--output_base=.local/bazel-out, or - Use an absolute/anchored path example (e.g., repo-root path) so the output always ends up under the ignored
/.local/directory.
| PR 17753 (2026-07-07) |
[reliability] BiDi init aborts session
BiDi init aborts session
RemoteWebDriver.startSession eagerly calls createBiDi() when the request has `webSocketUrl=true`, but createBiDi throws BiDiException if the returned `webSocketUrl` capability is missing or not a ws/wss String. This can fail driver construction even though the WebDriver session was created successfully (e.g., servers that return boolean `webSocketUrl` or omit it when unsupported).RemoteWebDriver.startSession() may throw after successfully creating a WebDriver session because createBiDi() throws when the returned webSocketUrl capability isn’t a valid ws:///wss:// string.
Some parts of the codebase already account for implementations returning non-string values for webSocketUrl (e.g., boolean), or removing the capability when BiDi isn’t supported. With this PR, requesting webSocketUrl: true can now turn those cases into a hard failure during driver construction.
- java/src/org/openqa/selenium/remote/RemoteWebDriver.java[291-296]
- java/src/org/openqa/selenium/remote/RemoteWebDriver.java[436-458]
- Make
createBiDi()non-throwing for missing/invalidwebSocketUrland returnOptional.empty()(optionally log at WARNING/FINE). - If you still want an explicit failure, move it to
getBiDi()(or overridegetBiDi()inRemoteWebDriver) so session creation succeeds but BiDi access fails with a clear message when BiDi is requested but unavailable. - Consider tracking a
bidiRequestedflag (based on the requested capabilities) to produce an accurate exception message whengetBiDi()is called.
[correctness] Case-sensitive ws URL check
Case-sensitive ws URL check
`RemoteWebDriver.createBiDi()` validates `webSocketUrl` using a case-sensitive `startsWith("ws://"|"wss://")`, so a valid WebSocket URI with an uppercase scheme (or leading whitespace) will be rejected and BiDi will not be initialized (only a warning is logged). This can cause BiDi to be unexpectedly unavailable even when the remote end returned a usable endpoint string.RemoteWebDriver.createBiDi() currently checks the WebSocket URL via a case-sensitive string prefix check (startsWith("ws://") / startsWith("wss://")). Since URI schemes are treated case-insensitively elsewhere in the codebase, this strict check can reject valid endpoints (e.g., WS://...) and prevent BiDi from being created.
The code already constructs a URI and handles URISyntaxException. The scheme validation should be derived from the parsed URI (or at least made case-insensitive + whitespace-tolerant) rather than relying on String.startsWith.
- java/src/org/openqa/selenium/remote/RemoteWebDriver.java[436-440]
Suggested implementation sketch:
- If
rawUrlis aString, trim it. - Parse it with
new URI(trimmed). - Validate
uri.getScheme()viaequalsIgnoreCase("ws") || equalsIgnoreCase("wss"). - If invalid, log + return
Optional.empty()as today.
[reliability] Leaked BiDi HttpClient
Leaked BiDi HttpClient
RemoteWebDriver.createBiDi() allocates a new HttpClient and then constructs a BiDi Connection, but if websocket establishment throws (e.g., ConnectionFailedException), the newly-created HttpClient is never closed. This can leak resources/threads on repeated failures and fails driver construction after the WebDriver session was already created.RemoteWebDriver.createBiDi() creates a new HttpClient (wsClient) and then opens a BiDi websocket via new Connection(wsClient, ...). If websocket opening fails (runtime ConnectionFailedException from HttpClient.openSocket), createBiDi() currently lets the exception propagate without closing wsClient, leaking resources.
-
org.openqa.selenium.bidi.Connectionopens the websocket in its constructor. -
JdkHttpClient.openSocketthrowsConnectionFailedExceptionon connection/setup failures.
- java/src/org/openqa/selenium/remote/RemoteWebDriver.java[435-459]
- Wrap the
Connection/BiDiconstruction in atry { ... } catch (RuntimeException e) { wsClient.close(); throw e; }. - Optionally, if the desired behavior is “BiDi requested but can’t connect => continue without BiDi”, catch
ConnectionFailedException, log at WARNING, closewsClient, and returnOptional.empty()instead of throwing.
[security] Logs BiDi URL value
Logs BiDi URL value
RemoteWebDriver.createBiDi() logs the full returned `webSocketUrl` when URI parsing fails, which can include session identifiers and a potentially connectable BiDi endpoint in application logs. This is sensitive on Grid where `webSocketUrl` commonly embeds `/session//...` in the URL path.RemoteWebDriver#createBiDi() logs the full webSocketUrl string on URISyntaxException. Since the URL commonly contains a session id and endpoint path, emitting it at WARNING level can leak sensitive session-specific data into logs.
This warning is triggered when BiDi was requested and the remote end returns a malformed webSocketUrl capability. Grid commonly returns URLs like ws://<host>:<port>/session/<id>/bidi.
- java/src/org/openqa/selenium/remote/RemoteWebDriver.java[451-454]
- Remove the
+ webSocketUrlconcatenation from the WARNING log, or log a redacted/sanitized version (e.g., scheme+host+port only; strip path/query), and optionally log the exception viaLOG.log(Level.WARNING, ..., e)without printing the raw URL.
| PR 17747 (2026-07-03) |
[reliability] Milestone lookup IndexError
Milestone lookup IndexError
chrome_for_milestone() indexes the filtered version list with [-1] and never checks HTTP status, so missing milestones or non-JSON upstream responses will fail with IndexError/JSONDecodeError instead of a clear, actionable error.scripts/chrome_version.py fetches JSON from upstream endpoints and immediately parses/selects data without validating HTTP status codes or guarding against an empty milestone match. This can crash automation with IndexError (no matching milestone) or JSONDecodeError (e.g., HTML error body), producing low-signal failures.
This module is shared by pinned_browsers and update_cdp, so an unclear failure here blocks scheduled pinning and release workflows.
- scripts/chrome_version.py[18-25]
- scripts/chrome_version.py[28-35]
- scripts/chrome_version.py[38-52]
- Check
r.statusfor eachhttp.request(...)and raiseValueErrorwith the URL and status when non-200. - In
chrome_for_milestone, materialize the filtered list and raise aValueErrorlikeNo Chrome-for-Testing versions found for milestone Xrather than indexing[-1]. - Optionally, validate expected JSON shape (e.g., presence of
versions/channels[channel]) to fail with clearer errors.
[correctness] `--chrome_channel` restricts values
`--chrome_channel` restricts values
The new `choices=["Stable", "Beta", "Dev", "Canary"]` constraint changes the script’s CLI contract by rejecting any other previously accepted `--chrome_channel` values (e.g., case variants used in automation). This can break existing callers and violates the backward-compatibility requirement for public interfaces.scripts/update_cdp.py now enforces a fixed set of --chrome_channel values via choices=..., which can break existing automation that previously passed other (but functionally equivalent) values such as different casing.
This script is commonly used in CI/automation; CLI flags are a de facto public interface. The new strict validation is a behavior change that can be made backward-compatible by normalizing inputs (e.g., case-insensitive mapping) or expanding accepted aliases.
- scripts/update_cdp.py[210-215]
[reliability] Unchecked milestone HTTP status
Unchecked milestone HTTP status
latest_for_channel() fetches Chrome-for-Testing JSON and immediately parses it without checking response status, so upstream 404/500/proxy error bodies can surface as JSONDecodeError/KeyError instead of a clear “fetch failed” exception. This reduces debuggability and is inconsistent with the newly-hardened fetch helpers elsewhere in the same scripts.latest_for_channel() performs http.request() and immediately json.loads(r.data) without verifying r.status. When Chrome-for-Testing endpoints return non-200 bodies (404, 500, proxy HTML, etc.), the scripts fail with parsing/KeyError exceptions that lack the URL and HTTP status.
Other fetch paths in these scripts were hardened to explicitly raise on non-200, so this new path should behave similarly to keep failures actionable.
- scripts/pinned_browsers.py[30-39]
- scripts/update_cdp.py[17-26]
- After each
http.request(...), checkresp.status != 200and raise aValueErrorthat includes HTTP status and URL (similar tocalculate_hash()/fetch_and_save()). - Consider reading/including a short snippet of the body (optional) if it helps diagnose auth/proxy issues, but don’t log full bodies if they might be large.
[reliability] Case-sensitive channel lookup
Case-sensitive channel lookup
latest_for_channel() indexes the upstream JSON using the provided channel string without normalization/validation, so update_cdp can crash with a KeyError when users pass values like "stable"/"beta" instead of "Stable"/"Beta".latest_for_channel(channel) performs a case-sensitive dictionary lookup (channels[channel]). When update_cdp.py passes a user-provided --chrome_channel value through unchanged, common casing variants (e.g., stable, BETA) crash the script with KeyError.
This is a CLI-exposed footgun: the error is not actionable and will look like an internal failure instead of invalid input.
- scripts/chrome_version.py[14-27]
- Normalize channel input (e.g., map lowercase to canonical keys) or validate against the keys returned by
last-known-good-versions.json. - On mismatch, raise
ValueErrorwith a clear message listing allowed channels.
[reliability] HTTP response not released
HTTP response not released
calculate_hash() streams downloads with preload_content=False but never releases the urllib3 connection, which can leak pooled connections across multiple large downloads in a pinning run.scripts/pinned_browsers.py:calculate_hash() streams response bodies (preload_content=False) but does not call release_conn() (or close the response) after reading. This can retain connections in the pool longer than needed and potentially exhaust the pool during the script's many downloads.
The repo already has a similar streaming download pattern that explicitly releases the connection.
- scripts/pinned_browsers.py[20-28]
- Wrap the streaming read in
try/finallyand callr.release_conn()in thefinallyblock. - If
r.status != 200, release the connection before raising.
Example structure:
r = http.request(..., preload_content=False)
try:
if r.status != 200:
raise ...
for chunk in ...:
...
return ...
finally:
r.release_conn()| PR 17743 (2026-07-02) |
[correctness] Unchecked regex rewrites
Unchecked regex rewrites
update_pin() rewrites common/webref_cddl.bzl with re.sub() but never verifies that the _COMMIT and _CDDL_FILES patterns matched, so the script can report success while leaving stale pins if the file format changes. This can silently break the intended “refresh pinned CDDL” behavior in release automation.update_pin() uses re.sub() for _COMMIT and _CDDL_FILES updates but does not check whether replacements occurred. If common/webref_cddl.bzl is reformatted (even slightly), the updater can become a no-op without failing.
update_module() already uses re.subn() and validates count == 1, showing the intended robustness pattern.
- scripts/update_cddl.py[100-111]
- scripts/update_cddl.py[113-125]
- Replace the two
re.sub(...)calls withre.subn(...)and assertcount == 1for each substitution. - If either count is not 1, raise a
RuntimeErrorthat includes which pattern failed (and ideally the file path) so CI failures are actionable. - (Optional hardening) Make the regexes whitespace-tolerant (e.g.,
\s*) and/or add explicit sentinel comments incommon/webref_cddl.bzlto anchor the generated block.
| PR 17739 (2026-07-02) |
[observability] EOF shown as U+FFFF
EOF shown as U+FFFF
In nextNumber(), several exception messages cast input.peek() (an int) to char; when the stream is exhausted this renders EOF (-1) as '\uFFFF', producing misleading diagnostics for inputs like "-", "5.", or "1e". This is especially confusing because the codebase explicitly treats U+FFFF as a valid literal character, so the error text can look like it saw a real character rather than end-of-input.JsonInput.nextNumber() builds error messages by casting input.peek() (an int) to char. When peek() returns Input.EOF (-1), the cast becomes \uFFFF, so errors at end-of-input misleadingly report that a U+FFFF character was seen.
Input.EOF is intentionally -1 to avoid colliding with any UTF-16 code unit, including U+FFFF; and there is a unit test ensuring U+FFFF is treated as a literal character.
- java/src/org/openqa/selenium/json/JsonInput.java[223-281]
- java/src/org/openqa/selenium/json/Input.java[31-38]
- Add a small helper (e.g.,
describeChar(int c)) that returns"<EOF>"whenc == Input.EOF, otherwise returns a quoted printable character. - Use that helper in the three
JsonExceptionmessages added innextNumber()(expected digit, expected digit after '.', expected exponent digit), so EOF is reported clearly as EOF rather than\uFFFF.
| PR 17738 (2026-07-02) |
[correctness] hasNext non-idempotent
hasNext non-idempotent
After `hasNext()` consumes a comma separator, a subsequent `hasNext()` call (before the next element is read) will throw `Expected ',' or end...` because `containerHasElement` is never reset and remains `true` for the rest of the container. This can break callers that probe `hasNext()` multiple times per element (a common iterator-style usage) even on valid JSON like `[1,2]`.JsonInput.hasNext() now uses containerHasElement as a proxy for “a comma is required before the next element”. However, markElementRead() only ever sets the flag to true, and hasNext() never clears it after consuming a comma. This makes hasNext() non-idempotent: for valid input like [1,2], after reading 1, the first hasNext() consumes the comma and returns true, but a second hasNext() (before reading 2) sees a value token with seenElement==true and throws.
-
markElementRead()sets the current container flag totrueand never sets it back tofalse. -
hasNext()consumes the comma but does not updatecontainerHasElement, so the subsequent non-comma path treats the next value token as a missing-comma violation.
- java/src/org/openqa/selenium/json/JsonInput.java[318-346]
- java/src/org/openqa/selenium/json/JsonInput.java[534-574]
- java/test/org/openqa/selenium/json/JsonInputTest.java[312-356]
- Treat the per-container boolean as “needsSeparator” (or add a separate boolean/stack for that state).
- In
hasNext()when a comma is successfully consumed, clear the flag for the current container (set it tofalse) so repeatedhasNext()calls remain valid until an element is actually read.- e.g., add a helper like
markSeparatorRead()that sets the top boolean tofalse.
- e.g., add a helper like
- Add a regression test demonstrating idempotency:
- Parse
[1,2], read1, callhasNext()twice (both should betrue), then read2. - Same idea for objects:
{"a":1,"b":2}after readinga:1, callhasNext()twice before readingb.
- Parse
- Update the field comment/name if its semantics change (e.g., from “has seen an element” to “separator required / element just read”).
[reliability] Premature stack pop
Premature stack pop
endArray()/endObject() pop both `stack` and `containerHasElement` before validating the container type, so a mismatch exception leaves the parser state mutated and makes subsequent error handling/recovery unreliable.endArray() and endObject() remove entries from stack and containerHasElement before checking whether the removed Container matches the expected container type. If the expectation check fails, a JsonException is thrown after internal state has already been mutated.
This is especially relevant now that there are two parallel stacks to keep consistent; throwing after mutation makes debugging/recovery harder and can cause follow-on failures if callers catch exceptions.
- java/src/org/openqa/selenium/json/JsonInput.java[403-441]
Change endArray()/endObject() to validate using peekFirst() (or store the peeked value) and only removeFirst() from both deques after the validation passes. For example:
public void endArray() {
expect(JsonType.END_COLLECTION);
Container expectation = stack.peekFirst();
if (expectation != Container.COLLECTION) {
throw new JsonException(
"Attempt to close a JSON List, but a JSON Object was expected. " + input);
}
stack.removeFirst();
containerHasElement.removeFirst();
input.read();
}Apply the same pattern to endObject().
[correctness] Comma enforcement bypassable
Comma enforcement bypassable
Comma validation is implemented only in hasNext(), so callers that read multiple elements or object members via next*()/nextName() without calling hasNext() can still successfully parse invalid JSON like "[1 2]" or "{\"a\":1 \"b\":2}". This leaves a spec-compliance gap depending on call pattern, despite the PR’s intent to enforce separators.JsonInput enforces commas only in hasNext(). If a caller reads successive elements (arrays) or successive name/value pairs (objects) by calling nextNumber()/nextString()/nextName() directly without an intervening hasNext(), the parser can still accept inputs that are missing commas (e.g. [1 2], {"a":1 "b":2}).
-
peek()skips whitespace and can therefore see the next token immediately after a prior value. -
expect()validates token types and updates container state, but does not require or consume a comma separator between elements. - The new separator logic is only in
hasNext(), so it is easy to bypass by not callinghasNext().
- java/src/org/openqa/selenium/json/JsonInput.java[318-346]
- java/src/org/openqa/selenium/json/JsonInput.java[534-574]
Centralize separator validation/consumption in a helper that is invoked when starting to read a new element/property (e.g., from expect() for values and for NAME in objects). This should:
- If the current container has already seen an element/pair, require a comma before the next element/pair.
- Consume exactly one comma (if present) and then continue.
- Keep
hasNext()consistent with this logic (e.g., call the same helper or ensure separators aren’t double-consumed).
[maintainability] Object comma cases untested
Object comma cases untested
The new tests cover array missing/leading commas but do not add equivalent malformed-object cases, leaving object-specific comma enforcement behavior unprotected against regressions.There are new regression tests for array comma rules, but no tests asserting the same behavior for objects (missing comma between entries and leading comma in an object).
JsonInput.peek() returns JsonType.NAME vs JsonType.STRING depending on whether the parser is currently expecting a property name (isReadingName()), so object paths are sufficiently distinct to warrant direct coverage.
- java/test/org/openqa/selenium/json/JsonInputTest.java[387-463]
- java/src/org/openqa/selenium/json/JsonInput.java[128-166]
- java/src/org/openqa/selenium/json/JsonInput.java[561-563]
Add tests similar to the array ones, e.g.:
- Reject missing comma between object entries:
{"a":1 "b":2}should throw fromhasNext()after reading the first value. - Reject leading comma in object:
{,"a":1}should throw fromhasNext()immediately afterbeginObject().
| PR 17737 (2026-07-02) |
[reliability] U+FFFF literal in test
U+FFFF literal in test
JsonInputTest embeds U+FFFF as a literal glyph ("") in a Java string, which can be fragile across environments/editors when the Java source encoding isn’t explicitly pinned for the Bazel target. The same coverage can be achieved more portably by using a Java unicode escape ("\uFFFF") or constructing the string from a char.JsonInputTest contains a literal U+FFFF character (rendered as ) inside a Java string literal. This makes the test dependent on how the source file is encoded/handled by tooling, even though the test intent is simply “the input contains UTF-16 code unit 0xFFFF”.
- The Bazel target for these tests does not specify
-encodinginjavacopts, so source encoding is not explicitly pinned at the target level.
-
Replace the literal
occurrences with a portable Java escape (e.g."\"a\uFFFFb\""and expected"a\uFFFFb"), or build the string withnew String(new char[]{'a','\uFFFF','b'}). -
java/test/org/openqa/selenium/json/JsonInputTest.java[295-304]
-
java/test/org/openqa/selenium/json/BUILD.bazel[19-28]
| PR 17731 (2026-06-30) |
[correctness] Inbound nulls accepted
Inbound nulls accepted
Serialization::Record#from_json can accept explicit null for non-nullable fields because read(field, raw) returns nil without checking field.nullable, bypassing the outbound nil validation. This creates invalid typed objects and can later silently omit the field during re-serialization.Serialization::Record.from_json currently allows nil to populate fields that are declared non-nullable, because read(field, raw) returns early on raw.nil? without validating field.nullable. This violates the “strict on values” inbound behavior and can create record instances that cannot be constructed outbound.
Outbound construction (new) rejects nil for non-nullable fields via validate_values, but inbound construction (from_json) uses construct and relies on read/wire_value.
- rb/lib/selenium/webdriver/bidi/serialization/record.rb[92-105]
- rb/lib/selenium/webdriver/bidi/serialization/record.rb[118-125]
In read(field, raw), when raw.nil?:
- return
nilonly iffield.nullableis true - otherwise raise
Selenium::WebDriver::Error::WebDriverError(or a consistent parsing error) indicating a non-nullable field received null, including the field name for debugging.
[quality] `Serialization` module lacks tag
`Serialization` module lacks tag
The internal `Serialization` module is introduced without a YARD `# @api private` tag in the doc block directly above the module definition. This can cause internal runtime APIs to appear public/documented when they are intended to be private.The YARD doc block immediately above module Serialization does not include # @api private.
This module is part of the internal BiDi generated-protocol runtime and should be explicitly marked private per compliance requirements.
- rb/lib/selenium/webdriver/bidi/serialization.rb[23-26]
[quality] `BiDiGenerate` lacks `@api private`
`BiDiGenerate` lacks `@api private`
The internal generator module `BiDiGenerate` is introduced without a YARD `# @api private` tag directly above the module definition. This violates the requirement to explicitly mark internal Ruby APIs as private in YARD docs.BiDiGenerate is an internal code generator module but its YARD doc block is missing # @api private directly above module BiDiGenerate.
Compliance requires internal Ruby APIs to be marked with @api private in YARD docs so they are not treated as supported public surface area.
- rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb[24-36]
[testability] BiDi specs use RSpec mocks
BiDi specs use RSpec mocks
New unit specs use RSpec mocking (`instance_double`, `allow(...).to receive`) to simulate `WebDriver::WebSocketConnection`. This violates the requirement to avoid mocks in tests unless using real or contract-driven integrations, because hand-authored mocks can drift from the real interface/behavior.RSpec mocking (instance_double, allow(...).to receive) is used in new BiDi unit tests, violating the "avoid mocks" compliance requirement.
The compliance rule allows simple in-memory fakes that implement the same interface without a mocking framework, or contract-driven stubs/integration tests.
- rb/spec/unit/selenium/webdriver/bidi/transport_spec.rb[27-33]
- rb/spec/unit/selenium/webdriver/bidi/protocol_types_spec.rb[164-193]
- rb/spec/unit/selenium/webdriver/bidi/protocol_types_spec.rb[211-218]
[correctness] Integer allows floats
Integer allows floats
Serialization::Record inbound primitive validation treats schema primitive "integer" as Numeric, so non-integer floats (e.g., 1.5) are accepted for integer-typed fields and can be propagated/re-serialized as invalid integer values. This contradicts the generated type intent (e.g., RBS declares these fields as Integer) and weakens the PR’s stated “strict on values” inbound validation behavior.Serialization::Record validates inbound primitives using PRIMITIVE_TYPES, but it maps the schema primitive "integer" to Numeric, which allows floats like 1.5 to pass as “integer”. This can allow out-of-schema data to enter typed records and be re-serialized back to the wire.
Generated protocol records now carry primitive: 'integer' metadata (e.g., BluetoothManufacturerData#key), and generated RBS expects Integer for these fields. The unit spec currently codifies accepting 1.5 as valid for an integer field.
- rb/lib/selenium/webdriver/bidi/serialization/record.rb[164-177]
- rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb[304-313]
- Update integer validation to reject non-integral numerics. Options:
- Strict: accept only
Integer. - Lenient but correct: accept
IntegerandFloatonly whenraw.finite? && raw % 1 == 0.
- Strict: accept only
- Update/replace the spec that currently asserts
1.5is accepted for an integer field to instead expect an error (and optionally add a case that accepts1.0if you choose the “integral float allowed” approach).
[reliability] Schema path not runfiles-safe
Schema path not runfiles-safe
`verify-bidi-generated` passes the BiDi schema via `$(location ...)`, but `check_generated.rb` treats the argument as a directly readable runtime path; Bazel does not guarantee execpaths are present/valid at test runtime, so `File.read(schema_path)` can fail and break Ruby CI now that it runs `bazel test //rb/...`. Use a runfiles path (`$(rootpath ...)`/runfiles resolver) to make the test reliably locate the generated schema JSON.verify-bidi-generated passes the generated schema JSON path using $(location ...), but the Ruby verifier reads that argument as a normal filesystem path. In Bazel tests, execpaths (often under bazel-out/...) are not guaranteed to be directly readable at runtime; tests should prefer runfiles paths.
- The schema artifact
//javascript/selenium-webdriver:create-bidi-src_schemais produced as a generated output viajs_run_binary. - The verifier
check_generated.rbreads the schema usingFile.read(schema_path)after a minimalDir.pwd-based fallback. - Ruby CI now runs
bazel test ... //rb/..., so the new verify test will run in CI.
- rb/lib/selenium/webdriver/BUILD.bazel[47-61]
- rb/lib/selenium/webdriver/bidi/support/check_generated.rb[27-31]
- In
rb/lib/selenium/webdriver/BUILD.bazel, change the verifier args to use a runfiles path:- Replace
$(location //javascript/selenium-webdriver:create-bidi-src_schema)with$(rootpath //javascript/selenium-webdriver:create-bidi-src_schema)(or the appropriate runfiles macro supported by your Bazel config).
- Replace
- In
check_generated.rb, make schema path resolution explicitly runfiles-aware (optional but more robust), e.g. try:-
File.join(ENV['TEST_SRCDIR'], ENV['TEST_WORKSPACE'], schema_path)whenFile.exist?(schema_path)is false.
-
- Ensure the schema target remains in
data(it already is) so it is available in runfiles.
[correctness] Hash params lose explicit null
Hash params lose explicit null
Transport#serialize drops all nil values when params is a Hash, so callers using hash/passthrough serialization cannot send explicit wire null even though the serialization layer defines nil as “explicit null” and UNSET as “omit”. This silently changes request semantics for nullable fields whenever the Hash path is used.Transport#serialize treats nil the same as omitted for Hash params by rejecting nil values. This prevents sending explicit JSON nulls via the Hash path even though the runtime documents a semantic difference between nil (wire null) and UNSET (omit).
Typed Data objects can correctly emit null-vs-omit, but Hash-based/passthrough calls currently cannot. This is especially risky for any commands/events that are modeled as passthrough hashes or for direct Transport#execute usage.
-
Change the Hash serialization filter to drop only
Serialization::UNSET, notnil. -
Update any affected specs/callers to pass
Serialization::UNSETwhen omission is intended. -
rb/lib/selenium/webdriver/bidi/transport.rb[42-49]
-
rb/spec/unit/selenium/webdriver/bidi/transport_spec.rb[61-66]
-
rb/lib/selenium/webdriver/bidi/serialization.rb[24-33]
[reliability] Unknown union variant raises
Unknown union variant raises
Serialization::Union#from_json raises ArgumentError when an inbound payload doesn’t match any variant and the union has no fallback, which can break parsing when a browser adds new union arms. This conflicts with the stated design goal in the runtime to keep inbound parsing forward-compatible (i.e., avoid breaking on values newer than the schema).Serialization::Union#from_json currently raises when no variant matches and the union class has no fallback, which makes inbound parsing brittle when a browser sends a new/unknown union discriminator (or a new presence-based arm).
The Data.from_json path explicitly avoids inbound validation to remain forward-compatible; unions should follow the same resilience principle. Many generated unions do not define fallback, so an unknown discriminator currently crashes parsing.
-
Implement a non-raising inbound behavior for unmatched unions (e.g., return the raw
json_payloadhash for unknown cases, or introduce a genericUnknownwrapper type) while keepingbuildstrict for outbound. -
Consider rescuing
ArgumentError/NameErrorinfrom_jsonand returningjson_payloadwhen the union cannot be resolved. -
rb/lib/selenium/webdriver/bidi/serialization/union.rb[43-80]
[maintainability] Bridge uses private ivar
Bridge uses private ivar
Remote::BiDiBridge constructs BiDi::Transport by extracting BiDi’s internal @ws via instance_variable_get, coupling the bridge to BiDi’s private state. This is brittle if BiDi’s internal connection field/name/lifecycle changes.Remote::BiDiBridge currently reaches into Selenium::WebDriver::BiDi using instance_variable_get(:@ws) to build a BiDi::Transport. This creates a hidden coupling to BiDi internals.
BiDi owns the websocket connection in an instance variable and does not expose it as part of its API.
- rb/lib/selenium/webdriver/remote/bidi_bridge.rb[20-34]
- rb/lib/selenium/webdriver/bidi.rb[34-64]
Introduce an explicit API seam instead of instance_variable_get, e.g.:
- Add a private
BiDi#connectionreader (orBiDi#transport) returning the underlyingWebSocketConnection(or aBiDi::Transport) and use that fromBiDiBridge. - Alternatively, make
BiDi::Transportaccept aBiDiinstance and adapt to its publicsend_cmdAPI.
[maintainability] Weak transport param assertion
Weak transport param assertion
In protocol_spec, the example “passes an allowed value through to the transport” only asserts that send_cmd was called, so it will still pass even if the validated enum value (wait: 'complete') is not forwarded/serialized into params.The spec claims it verifies that an allowed enum value is passed through to the transport, but it only checks that send_cmd was called (no argument assertion). This reduces coverage and can miss regressions where the wait parameter is dropped.
Transport#execute forwards method: and serialized params: to send_cmd, so the spec should assert the outgoing params include the validated value.
- rb/spec/unit/selenium/webdriver/bidi/protocol_spec.rb[51-58]
Update the expectation to assert the call arguments, e.g.:
expect(connection).to have_received(:send_cmd)
.with(method: 'browsingContext.navigate', params: {'context' => 'c', 'url' => 'u', 'wait' => 'complete'})(or hash_including('wait' => 'complete') if other params may be added).
| PR 17729 (2026-06-29) |
[reliability] Leaked BiDi session
Leaked BiDi session
Remote::BiDiBridge#create_session calls super to create a remote session and then can raise while validating webSocketUrl, but Driver#create_bridge/Driver#initialize do not rescue/cleanup, leaving an orphaned session on the remote end. This can leak resources and cause flaky subsequent runs due to leftover sessions.Remote::BiDiBridge#create_session now raises if the returned webSocketUrl is invalid, but this happens after super has already created a WebDriver session. Because Driver#create_bridge/Driver#initialize don't rescue around bridge.create_session, the newly-created session is never deleted when this error is raised.
This is a failure-path regression: it only triggers when a remote end returns an invalid webSocketUrl (e.g., boolean true), but when it does it can leave orphan sessions on the remote server.
- rb/lib/selenium/webdriver/remote/bidi_bridge.rb[26-67]
- rb/lib/selenium/webdriver/common/driver.rb[71-76]
- rb/lib/selenium/webdriver/common/driver.rb[333-338]
Wrap the post-super validation/BiDi initialization in a rescue block that deletes the newly-created session (and closes the HTTP client) before re-raising. For example:
- In
BiDiBridge#create_session, aftersuper, attempt to computesocket_url = validated_socket_url. - If validation fails, call
execute(:delete_session)(guarded by presence of a session id) andhttp.close(rescuing the same QUIT_ERRORS patterns), then re-raise. Optionally also hardenBiDiBridge#quitto handle@bidibeing nil (e.g.,@bidi&.close) so cleanup paths don’t crash when initialization fails early.
[quality] `enable_bidi!` missing YARD docs
`enable_bidi!` missing YARD docs
`Safari::Options#enable_bidi!` is a newly added public method but lacks the required YARD doc block with tags. This makes the public API change harder to discover and use correctly.New/modified public API methods must be documented with YARD tags.
Safari::Options#enable_bidi! was added and changes Safari capability behavior, but it has no doc block.
- rb/lib/selenium/webdriver/safari/options.rb[48-51]
Add a YARD doc block above def enable_bidi! including:
- A short description line
-
@return(likely[void]or[self]depending on convention) - Any relevant
@raise(if applicable) - Any behavioral notes (e.g., also enables
safari:experimentalWebSocketUrl)
| PR 17728 (2026-06-28) |
[quality] Test docstring missing Args
Test docstring missing Args
`test_get_remote_connection_selects_browser_specific_handler` has parameters but its new docstring does not follow Google-style `Args:` documentation. This reduces consistency and makes the test contract harder to understand and maintain.The newly added test function docstring is not Google-style and does not document its parameters under an Args: section.
Compliance requires Google-style docstrings with Args:/Returns:/Raises: sections as applicable for new/modified public functions. This test has parameters (options, prepare_options, expected_handler) but no Args: section.
- py/test/unit/selenium/webdriver/remote/remote_connection_tests.py[643-648]
| PR 17727 (2026-06-28) |
[reliability] Public titles removed without deprecation
Public titles removed without deprecation
Several `public string` title members were removed from `DriverTestFixture` without a deprecation phase or guidance to a replacement, which violates the project deprecation/backward-compatibility policy. This can immediately break downstream consumers (including derived test classes) that referenced these public members and cause compile failures.The PR removes public fields from DriverTestFixture (e.g., macbethTitle, simpleTestTitle) without first deprecating them and providing guidance to a replacement, which is a breaking change for any consumers compiled against the previous assembly.
Project policy requires public APIs to be deprecated for a deprecation period before removal, and the deprecation should include an explicit message naming the replacement (commonly via [Obsolete(...)]). These identifiers were previously referenced from tests and could also be referenced by other test classes deriving from DriverTestFixture, so removing them can cause immediate compile failures.
- dotnet/test/webdriver/DriverTestFixture.cs[30-38]
| PR 17724 (2026-06-28) |
[reliability] `Urls` uses `EnvironmentManager.Instance`
`Urls` uses `EnvironmentManager.Instance`
The updated test fixture still derives `Urls` from the global singleton `EnvironmentManager.Instance`, preserving shared mutable test context and undermining safe parallel execution. This conflicts with the requirement to eliminate static global test context for parallelizable tests.EnvironmentManager.Instance is still used as a global singleton source of test context (now via Urls), which violates the parallelization goal of removing static/shared global state.
The compliance requirement explicitly calls out eliminating EnvironmentManager-style static/shared context to enable safe parallel test execution.
- dotnet/test/webdriver/BiDi/BiDiFixture.cs[34-34]
- dotnet/test/webdriver/DriverTestFixture.cs[28-28]
| PR 17723 (2026-06-28) |
[reliability] Uncaught parseLong overflow
Uncaught parseLong overflow
Version.compare now parses digit-only segments via Long.parseLong without guarding against overflow, so large numeric components can throw NumberFormatException during comparisons. VersionCommand.getDockerProtocol does not catch NumberFormatException, so this can break Docker API version negotiation at runtime if Docker returns an unexpectedly large numeric ApiVersion/MinAPIVersion segment.Version.compare() treats digit-only components as numbers and calls Long.parseLong() (via toLong()), but no longer catches NumberFormatException. If a numeric segment overflows long (or otherwise fails to parse), version comparison throws and can bubble up into Docker protocol selection.
This class is used by VersionCommand.getDockerProtocol() to compare Docker API versions returned by /version. That method’s catch block does not include NumberFormatException, so a thrown parse exception can escape and fail protocol negotiation.
- java/src/org/openqa/selenium/docker/Version.java[91-115]
- java/src/org/openqa/selenium/docker/VersionCommand.java[86-113]
In compare() when both segments are numeric:
- Either wrap
parseLongin a try/catch and onNumberFormatExceptionfall back to a safe numeric comparison that doesn’t overflow (e.g., compare by string length, then lexicographically), or - Parse using
BigIntegerfor numeric segments.
Add a unit test covering very large digit-only segments (bigger than Long.MAX_VALUE) to ensure comparisons do not throw and maintain consistent ordering.
| PR 17717 (2026-06-25) |
[maintainability] Status line uses HTML comment
Status line uses HTML comment
The new charter includes HTML comments that mainly restate allowed values/placeholder instructions rather than capturing rationale, which reduces maintainability. Prefer making this guidance explicit prose (or explaining why these fields exist) instead of hidden comments.docs/plans/selenium-5.md uses HTML comments for status/options and a PR link placeholder. These comments communicate "what" to fill in, but do not explain rationale and are easy to miss.
Compliance prefers comments/documentation to explain intent/rationale rather than restating obvious mechanics or hiding required fields in HTML comments.
- docs/plans/selenium-5.md[3-5]
[maintainability] Undefined ADR reference
Undefined ADR reference
The “Partial BiDi implementation support” section says it is “Part of the Classic-migration ADR,” but this charter uses “classic-over-BiDi migration” terminology and the phrase “Classic-migration ADR” appears nowhere else, making the intended cross-reference unclear.The charter references a "Classic-migration ADR" that is not defined elsewhere in the document and does not match the surrounding terminology ("classic-over-BiDi migration"), reducing searchability and making future linking ambiguous.
This is in the Out of scope section under "Partial BiDi implementation support".
- docs/plans/selenium-5.md[47-55]
Replace “Classic-migration ADR” with terminology that matches the charter section title (e.g., “classic-over-BiDi migration ADR”), and optionally make it an explicit intra-doc link to the “Full classic-over-BiDi migration” section.
| PR 17714 (2026-06-24) |
[correctness] Java 11 incompatible test
Java 11 incompatible test
EitherTest calls `Stream.toList()`, but the repo’s default Bazel compilation target is `--release 11`, where `Stream.toList()` does not exist. This will fail compilation for the new test under the default build configuration.java/test/org/openqa/selenium/internal/EitherTest.java uses either.stream().toList(). Stream.toList() was introduced in Java 16, but this repo’s Bazel defaults to --release 11, so the test will not compile under the default toolchain/CI settings.
Bazel is configured to target Java 11 by default (--javacopt="--release 11"). New tests should avoid APIs introduced after Java 11 unless the test target explicitly compiles with a higher --release.
- java/test/org/openqa/selenium/internal/EitherTest.java[22-40]
- .bazelrc[23-35]
Update the test to avoid Stream.toList() (e.g., either.stream().collect(Collectors.toList())) and add the necessary Collectors import, or use Collectors.toUnmodifiableList() if you want closer behavior to Stream.toList() while still staying within Java 11.
| PR 17713 (2026-06-24) |
[quality] `match(matchAgainst, prefix)` Javadoc incomplete
`match(matchAgainst, prefix)` Javadoc incomplete
The modified public method `match(@Nullable String matchAgainst, @Nullable String prefix)` lacks a complete Javadoc block (no purpose sentence and no `@param` tags for `matchAgainst` and `prefix`). This violates the requirement that all changed public API methods have complete Javadoc, reducing API clarity and documentation quality.The public method match(@Nullable String matchAgainst, @Nullable String prefix) was modified, but its Javadoc is incomplete: it lacks a free-text purpose description and is missing @param tags for both parameters.
Compliance requires that any changed public API method in non-test code has complete Javadoc including a descriptive sentence and correct tags.
- java/src/org/openqa/selenium/remote/http/UrlTemplate.java[143-155]
| PR 17705 (2026-06-22) |
[correctness] Multiple enumerators allowed
Multiple enumerators allowed
EventStream.ReadAllAsync only blocks multiple ReadAllAsync calls, but the returned IAsyncEnumerable can still be enumerated multiple times (multiple GetAsyncEnumerator calls), which can create multiple readers against a Channel configured with SingleReader=true and lead to undefined behavior/lost events.ReadAllAsync returns an async-iterator IAsyncEnumerable<T> that can be enumerated multiple times even though the underlying channel is configured with SingleReader = true. The current _enumerating guard only prevents calling ReadAllAsync twice, not calling GetAsyncEnumerator twice on the same returned enumerable.
This can lead to multiple concurrent reads from the same ChannelReader<T> (violating the single-reader optimization/assumption), potentially causing missed events or other undefined behavior.
- dotnet/src/webdriver/BiDi/EventStream.cs[64-105]
- Return a custom
IAsyncEnumerable<TEventArgs>implementation (not an async-iterator directly) whoseGetAsyncEnumerator(...):- Uses an
Interlocked.CompareExchangeguard to allow only a single enumerator to be created. - Creates any linked cancellation token source inside
GetAsyncEnumeratorand ensures it is disposed when the enumerator is disposed.
- Uses an
- Alternatively (if intended), change semantics to allow multiple enumerations by removing
SingleReader = trueand making the channel multi-reader safe (but that likely changes stream semantics).
[correctness] `IBiDi.StreamAsync` return type changed
`IBiDi.StreamAsync` return type changed
The public `StreamAsync` APIs now return `Task>` instead of `Task>`, which is a source/binary breaking change for existing consumers. The prior event-stream abstraction is removed/invalidated without a deprecation phase and replacement guidance.Public BiDi streaming APIs were changed incompatibly (IEventStream<TEventArgs> -> IAsyncEnumerable<TEventArgs>), and the old surface is not preserved with a deprecation phase.
This is a user-visible .NET API change that will break existing callers at compile-time and can break binaries. The compliance policy requires backward-compatible public API/ABI, and separately requires deprecating public APIs with guidance before removal.
- dotnet/src/webdriver/BiDi/IBiDi.cs[68-70]
- dotnet/src/webdriver/BiDi/BiDi.cs[120-130]
- Reintroduce a compatibility surface for existing callers, e.g. keep
StreamAsyncreturningTask<IEventStream<TEventArgs>>(or add a differently-named method for the newIAsyncEnumerable<TEventArgs>return type, since return-type-only overloads are not possible in C#). - Mark the legacy API with
[Obsolete("Use <replacement API> instead.")]and keep it for at least one release cycle per project policy. - Ensure the replacement API is clearly discoverable (docs + IntelliSense) and tests cover both paths during the deprecation period.
[reliability] Undisposable stream leaks subscription
Undisposable stream leaks subscription
StreamAsync now returns IAsyncEnumerable, so callers cannot DisposeAsync the underlying EventStream even though SubscribeReaderAsync subscribes on the wire immediately. If a stream is created but never enumerated, the remote subscription remains registered (and keeps dispatching) until BiDi shutdown, potentially accumulating unused subscriptions.StreamAsync returns IAsyncEnumerable<T> (not disposable), but SubscribeReaderAsync performs an eager wire subscribe and registers the subscription in dispatcher slots. If the returned stream is never enumerated, there's no way for callers to trigger unsubscription, so the subscription stays active until the whole IBiDi instance is disposed.
-
EventDispatcher.SubscribeReaderAsynceagerly subscribes and adds theEventStreamto slots. -
EventStreamonly unsubscribes inDisposeAsync, which is no longer reachable via the public return type.
- Implement a lazy
IAsyncEnumerable<T>wrapper so no wire subscribe happens until the first enumeration (and dispose/unsubscribe happens when the enumerator is disposed). - Ensure the wrapper disposes the underlying
EventStreamif enumeration starts, and does nothing if it never starts.
- Change
BiDi.StreamAsync(...)(andEventSource/ContextEventSource.StreamAsync) to return anIAsyncEnumerable<T>that performs theSubscribeReaderAsync(...)call inside the enumerator (e.g., lazy-init on firstMoveNextAsync). - Keep
EventDispatcher.SubscribeReaderAsyncandEventStreammostly as-is; the key is to avoid calling them before enumeration.
- dotnet/src/webdriver/BiDi/BiDi.cs[120-130]
- dotnet/src/webdriver/BiDi/EventSource.cs[47-50]
- dotnet/src/webdriver/BiDi/ContextEventSource.cs[51-54]
- dotnet/src/webdriver/BiDi/EventDispatcher.cs[92-113]
- dotnet/src/webdriver/BiDi/EventStream.cs[89-107]
[reliability] Linked CTS can leak
Linked CTS can leak
ReadAllAsync eagerly creates a linked CancellationTokenSource but only disposes it inside the async iterator's finally; if the caller obtains the returned IAsyncEnumerable but never starts enumeration, the linked CTS (and its token registrations) is never disposed.ReadAllAsync allocates a linked CancellationTokenSource immediately, but disposal occurs only in the async-iterator finally. If the returned IAsyncEnumerable is never enumerated, the iterator never runs and the CTS is not disposed.
This leaks token registrations and can keep objects alive longer than necessary. It is especially relevant because IAsyncEnumerable is commonly used in deferred-execution pipelines.
- dotnet/src/webdriver/BiDi/EventStream.cs[73-104]
- Defer
CancellationTokenSource.CreateLinkedTokenSource(...)until enumeration actually begins (e.g., insideGetAsyncEnumeratorof a custom enumerable, or at the start of the iterator body). - Ensure the CTS is disposed deterministically when enumeration ends or the enumerator is disposed.
- If you implement the custom single-use enumerable from the other finding, fold this fix into that implementation so the CTS is only created when the enumerator is created.
[correctness] Stream silently completes on reuse
Stream silently completes on reuse
UnsubscribingAsyncEnumerator.DisposeAsync always disposes the owning EventStream (wire unsubscribe + channel completion), so the returned IAsyncEnumerable becomes single-use. Subsequent calls to GetAsyncEnumerator will read from a completed channel and terminate immediately, which can hide consumer mistakes and contradicts the Channel’s SingleReader optimization.The new enumerator wrapper disposes the owning EventStream whenever an enumerator is disposed. This makes the returned IAsyncEnumerable<T> single-use; re-enumeration completes immediately (because the channel is completed) rather than failing fast, which can silently mask bugs.
-
UnsubscribingAsyncEnumerator.DisposeAsync()calls_owner.DisposeAsync(). -
EventStream.DisposeAsync()completes the channel. -
GetAsyncEnumerator()does not check_disposedor enforce single enumeration.
- Fail fast on misuse (e.g., throw
ObjectDisposedExceptionif_disposed != 0). - Optionally enforce "single active enumerator" (since the channel is configured
SingleReader = true), e.g., track an_enumeratorCreatedflag and throw if called twice.
- dotnet/src/webdriver/BiDi/EventStream.cs[34-36]
- dotnet/src/webdriver/BiDi/EventStream.cs[63-76]
- dotnet/src/webdriver/BiDi/EventStream.cs[89-107]
- dotnet/src/webdriver/BiDi/EventStream.cs[110-141]
| PR 17700 (2026-06-21) |
[correctness] Orphaned synthetic variant defs
Orphaned synthetic variant defs
canonicalizeVariantParams() appends synthesized variant-record defs to `created` before confirming all choice branches are supported, so a later bailout (`continue`) can leave behind unreferenced synthetic defs while keeping the original def unchanged. This makes the normalized AST (and therefore the projected schema) depend on branch order and can introduce unexpected extra types.canonicalizeVariantParams() mutates output even when it decides not to canonicalize a detected inline-variant record. It pushes synthesized variant defs into created during the per-branch map, but if any branch is unsupported it bails out (continue) and leaves the original def untouched. This results in orphan synthetic defs being appended to the output AST.
The current control flow creates defs before validating that all branches are supported.
- javascript/selenium-webdriver/normalize_bidi_ast.mjs[327-367]
Refactor to be atomic per def:
- First, pre-validate and compute all
branchType()entries fordetected.branches. - If any entry is null/unsupported, do not create any synthesized defs and do not modify
def. - Otherwise, build synthesized defs into a temporary local array (e.g.,
newDefs) and only then append them tocreatedand rewritedeftoType='variable'. - Add a unit test covering an inline-variant record where one branch is unsupported, asserting that no synthesized defs are added to the output.
[correctness] Null-only types become unknown
Null-only types become unknown
In projectRef(), `null` and prelude `nil` alternatives are filtered out before projecting the remaining type; if the input type is *only* `null`/`nil`, the remaining list is empty and the schema emits `{ primitive: 'unknown', nullable: true }` instead of a real null type. This can silently corrupt schema fidelity for any CDDL field/alias defined as exactly `null`/`nil`, and the cddl2ts diff check likely won’t catch it because it only asserts presence of nullability, not the underlying primitive.projectRef() removes null/nil alternatives via isNullAlt(), then projects entries[0]. When the type is exactly null/nil (no non-null alternatives), entries becomes empty and projectEntry(undefined) returns { primitive: 'unknown' }, producing an incorrect schema node.
-
PRELUDE.nilis mapped to'null', andprojectEntry()can correctly project anilgroup ref to{ primitive: 'null' }. - But
isNullAlt()strips it before projection, which is only correct when there is at least one non-null alternative.
- javascript/selenium-webdriver/project_bidi_schema.mjs[73-87]
- In
projectRef(type), after computingentries, add a guard:- If
entries.length === 0andall.length > 0, return{ primitive: 'null' }(and do not setnullable), because the type is exactly null. - Only set
node.nullable = truewhen there is at least one non-null alternative (i.e.,entries.length > 0 && entries.length < all.length).
- If
- Optionally add/extend a unit test to cover
nil-only andnull-only projection cases.
[correctness] Alias refs unchecked
Alias refs unchecked
`checkSchema()` never traverses `kind: 'alias'` nodes, so an alias whose RHS contains a `{ref: ...}` can point at a missing type and still pass validation. This breaks the “fail-closed” schema gate by allowing dangling references to ship undetected.checkSchema() validates refs for command/event params/results, record fields, and union variants, but it does not validate references contained inside kind: 'alias' nodes (the alias RHS). This allows unresolved/dangling refs to pass schema validation.
Aliases are produced by projectType() for variable defs that are not pure enums and not unions of refs.
- javascript/selenium-webdriver/project_bidi_schema.mjs[106-116]
- javascript/selenium-webdriver/project_bidi_schema.mjs[166-200]
- In
checkSchema(), add a branch fornode.kind === 'alias'that runsrefsIn(node.type)and errors on any ref that does not exist inschema.types. - Ensure
refsIncan traverse the alias RHS as-is (it already handlesref/list/map/union/recordtype-ref shapes).
[correctness] Record map refs unchecked
Record map refs unchecked
`projectRecord()` can emit typed maps via `record.map` (from `* text => T`), but `checkSchema()` only validates `record.fields` and ignores `record.map`. As a result, unresolved refs inside the map value type won’t be caught by schema validation.Issue description
projectRecord() stores typed-map value types under record.map, but checkSchema() does not validate refs inside record.map. This allows dangling type references to slip through the schema gate for records that use * text => SomeType.
Issue Context
The schema vocabulary explicitly supports maps on records, and projectRecord() sets record.map = value when the wildcard entry is not any.
Fix Focus Areas
- javascript/selenium-webdriver/project_bidi_schema.mjs[126-145]
- javascript/selenium-webdriver/project_bidi_schema.mjs[166-200]
Proposed fix
- In
checkSchema(), within thenode.kind === 'record'branch, also validatenode.mapif present:-
for (const r of refsIn(node.map)) if (!has(r)) errors.push(${name}: unresolved map value type ${r})
-
- Keep existing
fieldsvalidation unchanged.
[reliability] Allocator reserves names on bailout
Allocator reserves names on bailout
In canonicalizeVariantParams(), synthetic variant names are allocated (mutating the shared allocator) before verifying that all choice branches are supported; if the function bails out, those allocated names remain reserved even though no defs were emitted. This can cause unrelated later synthetic types to pick up unexpected numeric suffixes and makes schema naming depend on unsupported-branch presence/order.canonicalizeVariantParams() calls the global alloc(...) name allocator while building memberRefs, before it knows whether it will commit the staged synthetic defs. If any branch is unsupported, the function bails out (continue) and drops stagedDefs, but the allocator’s internal taken set has already been mutated. This makes subsequent synthetic names change (e.g., gaining 2, 3, … suffixes) even though the earlier names were never emitted.
The new staging logic correctly avoids leaking orphaned synthetic defs, but name allocation still happens inside the staging loop, so the allocator still leaks state on bailout.
- javascript/selenium-webdriver/normalize_bidi_ast.mjs[332-365]
- While mapping branches, do not call
alloc(...). Instead, collect per-branch info (e.g.,{ localNameCandidate, fields, memberLabel, supersedes }) and track whether any branch is unsupported. - If any branch is unsupported, bail out without touching the allocator.
- Only after confirming all branches are supported, loop over the staged branch infos and call
alloc(...)to assign finalsynthNames and buildstagedDefs/memberRefs.
(Alternative: implement a transactional allocator that can rollback reserved names when bailing out, but the “allocate only on commit” approach is simpler and more robust.)
[correctness] Nullable drift unchecked
Nullable drift unchecked
In diffAgainstCddl2ts(), nullability mismatches are only reported when cddl2ts marks a field nullable but the schema does not; the reverse case (schema nullable but cddl2ts not) is never flagged, so accidental nullability expansion can slip past the fidelity gate.Issue description
diffAgainstCddl2ts() currently checks nullability in only one direction:
- it errors when
cddl2tssays a field is nullable and the schema is not - it does not error when the schema marks a field nullable but
cddl2tsdoes not
This weakens the fidelity gate by allowing schema-side nullability drift (e.g., mistakenly adding null to a field type) to pass.
Issue Context
This logic is in the record-field type fidelity loop that compares optional/nullable/array characteristics between the projected schema and the cddl2ts oracle.
Fix Focus Areas
- javascript/selenium-webdriver/bidi_schema_diff_test.mjs[285-299]
Suggested change
Add a symmetric check such as:
- if
!o.nullable && field.type?.nullablethen push an error like:${name}.${fname}: schema is nullable, cddl2ts is not
Keep the existing NULLABLE_DIFFERENCES allowlist behavior as-is for the known cddl2ts nullable, schema not direction (and let the existing stale NULLABLE_DIFFERENCES logic catch resolved allowlist entries).
| PR 17699 (2026-06-21) |
[quality] `assert_local_arguments` missing `@api private`
`assert_local_arguments` missing `@api private`
`assert_local_arguments` appears to be an internal helper but is introduced as a public method without a YARD `@api private` marker. This expands the public surface area unintentionally and violates the internal-API marking requirement.A new internal helper method (assert_local_arguments) is publicly exposed and lacks a # @api private YARD tag.
This helper is used only to validate constructor arguments for local drivers and is not intended as a supported API.
- rb/lib/selenium/webdriver/common/local_driver.rb[39-45]
[quality] `ClientConfig#initialize` lacks YARD
`ClientConfig#initialize` lacks YARD
The new public `ClientConfig` API is missing YARD `@param`/`@return` documentation for its initializer and accessors, making the public contract unclear for users. This violates the requirement to document public API methods with YARD tags.Selenium::WebDriver::ClientConfig appears to be a new public API (no @api private tag), but its methods (notably #initialize) lack the required YARD documentation (@param, @return, and any relevant @raise).
The compliance checklist requires all public API methods in changed code to be documented with YARD tags and a short description.
- rb/lib/selenium/webdriver/common/client_config.rb[24-74]
[testability] `instance_double` introduced in tests
`instance_double` introduced in tests
New/modified unit tests introduce RSpec mocking (`instance_double`) instead of using a real implementation or a simple in-memory fake. This violates the rule to avoid mocks in tests unless backed by contract-driven integrations.The changed tests use RSpec mocks (instance_double), which is disallowed by the compliance rule unless the mock is contract-backed; these tests can use a small fake object or a real HTTP client instance configured for the test.
This PR adds/changes tests around HTTP client/config plumbing; these behaviors can typically be validated with a minimal fake that implements the required interface without using a mocking framework.
- rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb[55-60]
- rb/spec/unit/selenium/webdriver/remote/http/default_spec.rb[67-86]
[quality] `proxy` param documents Hash
`proxy` param documents Hash
`ClientConfig#initialize` documents `proxy` as accepting a `Hash`, but it stores the value as-is while the HTTP stack later expects a `Proxy`-like object responding to methods such as `http` and `no_proxy`. This documentation/behavior mismatch misleads users and can cause runtime errors when a Hash is passed per the docs.ClientConfig#initialize YARD docs indicate proxy can be provided as a Hash, but the implementation stores the value directly and downstream HTTP transport code expects a Selenium::WebDriver::Proxy (or proxy-like object) that responds to http/no_proxy, leading to runtime errors for users who pass a Hash as documented.
Remote::Http::Default#use_proxy? and the HTTP-client construction path (e.g., #new_http_client) call proxy.http and proxy.no_proxy, which a Ruby Hash does not provide. Because this is a public API surface, users following the YARD docs may pass a hash such as {http: 'http://proxy:8080'} and encounter failures when the HTTP transport is built.
Implementation options (choose one):
-
Docs-only: change YARD to only accept
Proxy(keep current behavior). -
Support Hash: accept
Hashby converting it to aProxyduring initialization and/or assignment (e.g., ininitializeorproxy=), and update the YARD (and any related type definitions such as RBS) accordingly.
- rb/lib/selenium/webdriver/common/client_config.rb[42-64]
- rb/lib/selenium/webdriver/remote/http/default.rb[147-187]
[reliability] Redirect drops config headers
Redirect drops config headers
Remote::Http::Default#follow_redirect re-issues redirected requests using DEFAULT_HEADERS only, dropping ClientConfig-provided User-Agent and extra_headers that Common#common_headers otherwise applies. This makes ClientConfig header customization inconsistent across redirect chains.Redirect-following currently rebuilds headers with DEFAULT_HEADERS.dup, which omits client_config.user_agent and client_config.extra_headers that are included in normal requests.
Remote::Http::Common#common_headers is the centralized place that merges User-Agent and extra_headers from client_config. Redirects should preserve these headers for consistency.
- rb/lib/selenium/webdriver/remote/http/default.rb[126-131]
- In
follow_redirect, usecommon_headers.dupinstead ofDEFAULT_HEADERS.dupwhen callingrequest. - Ensure any redirect-specific header adjustments (if needed) are preserved.
| PR 17697 (2026-06-21) |
[correctness] Issue template link wrong
Issue template link wrong
docs/decisions/README.md tells users to open an ADR tracking issue using the template, but the link points to the template YAML file in the repo (it won’t launch the pre-filled new-issue form). Since blank issues are disabled, this can leave users unsure how to create the tracking issue correctly.docs/decisions/README.md instructs users to use the ADR Implementation Tracking issue template, but the current markdown link targets the template YAML file in .github/ISSUE_TEMPLATE/, which only shows the file contents and does not open the GitHub issue creation form.
This repo disables blank issues, so users are expected to create issues via templates.
- docs/decisions/README.md[44-48]
Update the link to point to the GitHub issue creation URL for the template, e.g.:
https://github.com/SeleniumHQ/selenium/issues/new?template=adr-tracking.yml
Optionally include a short instruction like “click New issue and choose ADR Implementation Tracking” if you prefer not to hardcode the full URL.
[maintainability] ADR filename steps unclear
ADR filename steps unclear
The new ADR PR template shows the record as `docs/decisions/NNNN-short-title.md`, but the documented process creates `short-title.md` first and renames to `NNNN-short-title.md` only after the PR number exists. This mismatch can confuse authors using the template about the expected initial filename/rename sequence.The ADR PR template currently presents only the final, numbered ADR filename, while the documented process expects authors to start with an unnumbered filename and rename after the PR number is assigned.
This is contributor-facing guidance. Aligning the PR template with the README process reduces confusion and avoids back-and-forth during PR creation.
- .github/PULL_REQUEST_TEMPLATE/adr.md[6-9]
| PR 17690 (2026-06-19) |
[maintainability] Import order not formatted
Import order not formatted
Some changed BiDi classes place `import org.openqa.selenium.Beta;` before `java.*` imports (e.g., `HasBiDi`), which is inconsistent with the repository’s google-java-format output. The CI “Format” job runs `./go format` (google-java-format) and fails if it produces diffs, so this will block CI until imports are reformatted.Some modified Java files have import blocks that are not in the canonical order produced by the repo’s formatter (google-java-format). This causes CI’s format check to modify files and then fail the job because the working tree is dirty.
CI runs ./go format, which applies google-java-format to all java/**.java files, and then fails if git diff is non-empty.
Run ./go format and commit the resulting changes, or manually reorder imports to match google-java-format output (then re-run ./go format to confirm no changes).
- java/src/org/openqa/selenium/bidi/HasBiDi.java[18-23]
- java/src/org/openqa/selenium/bidi/browser/ClientWindowInfo.java[18-23]
- java/src/org/openqa/selenium/bidi/browser/SetDownloadBehaviorParameters.java[18-27]
| PR 17689 (2026-06-18) |
[correctness] Wrong output_base invocation
Wrong output_base invocation
AGENTS.md says users can “pass `--output_base=.local/bazel-out`”, but in this repo `output_base` is handled as a Bazel startup option (via `startup --...`). Users following the docs may pass it as a normal build/test flag and Bazel will reject it, breaking the worktree setup guidance.AGENTS.md suggests users can alternatively “pass --output_base=.local/bazel-out”, but --output_base is a Bazel startup option (as shown elsewhere in the repo). This phrasing can lead users to pass it in the wrong position (as a command option), which Bazel will reject.
The repo already documents output_base correctly as a startup option in README.md, and .bazelrc uses startup stanzas for startup-only flags.
- AGENTS.md[42-46]
| PR 17678 (2026-06-12) |
[reliability] `clearAll` deletes completion marker
`clearAll` deletes completion marker
`RedisBackedNewSessionQueue.addToQueue` now unconditionally calls `safelyClearRedisState(reqId)` in a `finally` block, and that helper deletes the Redis completion marker (`completedKey`) and stored result keys via `clearAll`. Removing these guard/result keys can let a later/retried `complete(reqId, ...)` “win” again and/or return `tracked=false`, which can lead to incorrect result overwrites or upstream teardown decisions in concurrent/failover scenarios.addToQueue now always runs Redis cleanup in a finally block via safelyClearRedisState(reqId), and that cleanup calls clearAll(reqId) which deletes completedKey(reqId) and the result:* keys. These keys are required for the “winner-takes-all”/idempotent completion semantics used by complete() and for communicating the terminal result across retries/replicas; deleting them can allow late/duplicate complete() calls to succeed again or return tracked=false, triggering incorrect result overwrites or upstream teardown decisions.
The completion marker (completedKey) is intended to ensure only one terminal completion wins when timeouts race with success and to keep complete() idempotent across retries/replicas via a SET-NX guard. The recent change makes addToQueue exception-safe by always cleaning up Redis state, but the cleanup is now too aggressive because it removes the completion marker and stored result keys, effectively re-opening the request for a second completion in concurrent/failover scenarios.
- java/src/org/openqa/selenium/grid/sessionqueue/redis/RedisBackedNewSessionQueue.java[245-269]
- java/src/org/openqa/selenium/grid/sessionqueue/redis/RedisBackedNewSessionQueue.java[608-636]
[reliability] `addToQueue` not exception-safe
`addToQueue` not exception-safe
`RedisBackedNewSessionQueue.addToQueue` registers per-request state in in-memory registries (`waiters`, `contexts`) and performs multiple Redis side effects, but the cleanup of those registries/keys only happens on the normal path after `awaitResult` returns. If any Redis write or other runtime error occurs before that point, the method can leak per-request registrations and leave partial/stale Redis queue state behind, especially during transient Redis failures/outages.RedisBackedNewSessionQueue.addToQueue performs multi-step side effects (registering request state in contexts/waiters, writing Redis keys, pushing a queue entry) but only cleans up that state on the happy path after awaitResult returns. If a Redis operation (or any runtime exception) occurs before the normal cleanup block, the method can exit without removing the in-memory tracking entries and without best-effort cleanup of partially-created Redis keys/list entries, leaking memory and leaving stale queue state.
This is concurrent/resource-heavy, Redis-backed queue code expected to run in failure-prone environments (timeouts, connection resets, closed connections), so cleanup must be bounded and exception-safe across all terminal paths, including partial failures while enqueuing. The current implementation lacks a try/finally to guarantee removal from waiters/contexts, and does not ensure best-effort Redis cleanup when failures happen early.
- java/src/org/openqa/selenium/grid/sessionqueue/redis/RedisBackedNewSessionQueue.java[227-256]
| PR 17670 (2026-06-11) |
[maintainability] No binding status section
No binding status section
The ADR lacks the required "Binding status" section/table for tracking convergence, and instead tells readers tracking is in an issue linked from the PR. This conflicts with the documented ADR process and makes the record less self-contained for implementers.The ADR does not include the template-required ## Binding status section/table (with per-binding status and notes/tracking links). It also points readers to tracking “linked from this record’s PR” instead of embedding tracking in the record.
Repo documentation says bindings track convergence in the decision’s binding-status table, and the template provides the exact section/table to include.
- docs/decisions/17670-bidi-is-an-implementation-mechanism.md[17-23]
- docs/decisions/17670-bidi-is-an-implementation-mechanism.md[81-87]
- Add a
## Binding statussection at the end, using the template’s table columns (Binding | Status | Notes / tracking link). - Optionally repurpose the existing “Current behavior” details into the Notes column.
- Replace the Consequences bullet that points to “linked from this record’s PR” with an explicit link in the table (or in Consequences) so the record remains self-contained.
[maintainability] Index entry not linked
Index entry not linked
docs/decisions/.index.md lists ADR 0001 as plain text instead of a markdown link, so the “Index of Decisions” is not navigable. Readers can’t click through to the actual record file.docs/decisions/.index.md is intended to be an index, but the ADR entry is plain text rather than a markdown link to the decision record file, which makes the index non-navigable.
The ADR itself is stored in a slugged filename (0001-bidi-is-an-implementation-mechanism.md), so a link also removes ambiguity about the destination.
- docs/decisions/.index.md[1-3]
- docs/decisions/0001-bidi-is-an-implementation-mechanism.md[1-2]
[maintainability] Missing ADR date
Missing ADR date
The ADR header omits the required "Date" field, so readers can’t tell when the status last changed. This deviates from the project’s ADR template and reduces decision-log auditability.The ADR header is missing the - Date: YYYY-MM-DD field required by the ADR template.
The template specifies that the header must include a date indicating when the status last changed.
- docs/decisions/17670-bidi-is-an-implementation-mechanism.md[3-5]
Add a - Date: <YYYY-MM-DD> line (the date the status last changed) between Status and Discussion.
[correctness] Option 3 ambiguous
Option 3 ambiguous
In ADR 0001, considered option #3 lacks an explicit Accepted/Rejected marker and its explanation reads like the rationale for the *opposite* choice (hiding BiDi from users), leaving the decision record unclear about what was rejected and why. This makes the ADR less self-contained and increases the risk of misinterpreting the intended cross-binding direction.ADR 0001’s “Considered options” list contains an option (#3) that does not indicate whether it was accepted or rejected, and its rationale text appears to argue for the internal-only approach rather than for allowing BiDi references. This ambiguity reduces the ADR’s usefulness as a durable record.
The ADR template expects each considered option to state why it was accepted or rejected; the current option #3 neither labels its status nor clearly explains why it lost relative to option #4.
- docs/decisions/0001-bidi-is-an-implementation-mechanism.md[38-40]
- Mark option #3 explicitly as
(Rejected). - Rewrite its bullet to explain why allowing public API surfaces to reference BiDi was rejected (e.g., it exposes protocol details, couples the supported API to an implementation mechanism, harms portability/compatibility), and ensure it no longer reads like support for option #4.
[maintainability] Discussion link not PR
Discussion link not PR
ADR 0001’s “Discussion” field uses “PR pending” and links to an issue instead of linking to the ADR’s PR, which conflicts with the template/process that defines the PR thread as the discussion record. This weakens traceability from the ADR to its canonical review/discussion history.The ADR template/process expects the Discussion: field to link to the decision record’s PR (the canonical discussion record). ADR 0001 currently says _PR pending_ and links to an issue instead.
It’s fine to reference related issues, but per the template those should go under Context (or otherwise be secondary), while Discussion: should point at the ADR PR.
- docs/decisions/0001-bidi-is-an-implementation-mechanism.md[3-6]
- docs/decisions/0000-template.md[6-9]
- docs/decisions/README.md[30-36]
[maintainability] Hidden ADR index
Hidden ADR index
The ADR index is introduced as a dotfile (docs/decisions/.index.md) and is not referenced from docs/decisions/README.md, so readers who follow the documented ADR entry point won’t discover the index page. This reduces ADR discoverability and makes it harder to navigate the decisions log over time.The ADR index is stored in docs/decisions/.index.md but docs/decisions/README.md does not link to it, so readers landing on the ADR directory documentation won't find the index.
The ADR process documentation lives in docs/decisions/README.md, which serves as the primary entry point for how to use/consume ADRs in this repo.
- docs/decisions/README.md[1-45]
- docs/decisions/.index.md[1-3]
Do one of the following:
- Add a prominent link to
.index.mdnear the top ofREADME.md(e.g., “Index of Decisions”). - Rename
.index.mdtoindex.mdorINDEX.mdand link to it fromREADME.md. - Alternatively, merge the index list into
README.mdand drop the separate index file.
[maintainability] Untracked items claim
Untracked items claim
ADR 0001 says non-conforming public surfaces are “tracked below”, but the Binding status table provides no notes/tracking links for any binding, so the ADR doesn’t actually provide the promised tracking references.The ADR states that non-conforming public surfaces are “tracked below”, but the Binding status table has an empty “Notes / tracking link” column across all bindings, so there is no actual tracking information.
This is a documentation consistency/traceability problem inside the ADR itself.
- docs/decisions/0001-bidi-is-an-implementation-mechanism.md[44-56]
Either:
- Populate the “Notes / tracking link” cells with the relevant tracking issues/PRs per binding, or
- Remove/adjust the “tracked below” wording until concrete tracking links exist.
| PR 17668 (2026-06-11) |
[correctness] PKG empty name accepted
PKG empty name accepted
`uncompress_pkg()` validates `..`/absolute/prefixed paths but does not reject an empty `entry.name()`, so `target.join("")` resolves to the extraction root and `fs::write()` may create/overwrite the target path as a file instead of a directory. This can cause extraction failures or clobber the intended extraction directory when the target path does not yet exist.uncompress_pkg() calls check_path_traversal(Path::new(name)), but check_path_traversal() does not reject empty paths. If a CPIO entry name is "", then target_path.join(name) resolves to target itself, and fs::write(&target_path_buf, file) may create/overwrite the extraction root path as a file (especially when target doesn’t exist yet).
The tar extraction path already explicitly rejects an empty entry path; pkg/cpio should match that safety behavior.
- rust/src/files.rs[223-253]
- rust/src/files.rs[95-107]
[correctness] Tar first segment always dropped
Tar first segment always dropped
uncompress_tar() always does iter().skip(1) on every entry path, so single-component entries (e.g., "file.txt") become an empty path and are unpacked to `target` itself. This can overwrite the intended extraction directory path with a file or cause extraction failures for tarballs that don't include a top-level folder.uncompress_tar() unconditionally strips the first path component (iter().skip(1)), which can turn a valid tar entry path with a single component into an empty path. That causes entry_target to resolve to the extraction root (target) and unpack() may write a file at the target path or fail when the code expects target to be a directory.
unzip() already has logic to strip the first component only when the archive paths actually contain a parent folder; tar extraction should behave similarly.
- rust/src/files.rs[375-390]
- rust/src/files.rs[419-432]
- Mirror the
unzip()approach:- Count components (or otherwise detect whether there is a top-level folder).
- Only
skip(1)when there is more than one component. - Otherwise, keep the original path.
- Additionally, consider explicitly rejecting an empty
entry_pathfor regular-file entries to avoid writing directly totarget.
[security] Tar.gz bypasses validation
Tar.gz bypasses validation
`check_path_traversal()` is never applied to the `.gz` extraction path: `uncompress()` dispatches `GZ` to `untargz()`, which calls `Archive::unpack()` directly without validating archive entry paths. As a result, tar.gz remains outside the PR’s “reject unsafe entries” behavior, undermining the stated traversal prevention for tar extraction.The PR introduces check_path_traversal() and uses it in uncompress_pkg() and uncompress_tar(), but .tar.gz files are extracted via untargz() which does not validate entry paths.
uncompress() routes GZ to untargz(), and untargz() uses tar::Archive::unpack() without calling check_path_traversal() on each entry. This means the security behavior added in this PR is not applied to tar.gz.
- rust/src/files.rs[149-167]
- rust/src/files.rs[360-375]
- Refactor
untargz()to iteratearchive.entries()?and, for each entry:- read
entry.path()?(or equivalent), - apply
check_path_traversal()to that path, - unpack the entry into the intended destination (e.g., by joining to
parent_pathafter validation, or using an unpack-into-directory API if available).
- read
- Optionally add a unit test similar to
uncompress_tar_rejects_path_traversal_entry, but covering the.gzpath to prevent regressions.
[security] Absolute path escape
Absolute path escape
check_path_traversal() only rejects ParentDir (`..`) components, so absolute/prefixed entry paths pass validation. In uncompress_pkg(), an absolute `name` can cause `target_path.join(name)` to ignore `target_path` and write outside the extraction directory.check_path_traversal() currently only rejects .. path components. Absolute paths (Unix /etc/passwd) and Windows-style prefixed paths (e.g., C:\Windows\..., UNC) can still pass validation and escape the intended extraction root when joined with target.
uncompress_pkg() validates entry.name() with check_path_traversal(Path::new(name)) and then writes to target_path.join(name). If name is absolute/prefixed, the joined path may resolve outside target.
- rust/src/files.rs[95-103]
- rust/src/files.rs[219-237]
- Update
check_path_traversal()to reject any non-relative components, not justParentDir, e.g. fail onComponent::RootDirandComponent::Prefix(_)as well. - Optionally add a defensive post-join check in
uncompress_pkg()(and any similar extraction code): after building the output path, ensure it is not absolute and is lexically undertarget. - Add unit/integration tests (if available in this repo) covering entry names like
/tmp/evil,\\server\share\evil(Windows), andC:\evil.
[reliability] `check_path_traversal` lacks tests
`check_path_traversal` lacks tests
New validation rejects archive entries containing `..` components in `uncompress_pkg()` and `uncompress_tar()`, but this behavior change is not accompanied by targeted tests. This risks regressions and gaps in coverage for a security-sensitive extraction path.The PR adds path traversal validation during archive extraction, but does not add tests to confirm malicious paths are rejected and safe paths still extract.
The behavior change occurs in uncompress_pkg() and uncompress_tar() via check_path_traversal(). Because this is security-sensitive and error-path driven, tests should cover both allowed and rejected cases.
- rust/src/files.rs[95-103]
- rust/src/files.rs[219-236]
- rust/src/files.rs[371-386]
| PR 17659 (2026-06-09) |
[correctness] Mismatch error masked by cache
Mismatch error masked by cache
The new major-version mismatch branch returns an error, but `main.rs` can still convert that error into an OK exit when cached-driver fallback is enabled, so the intended fail-fast behavior for conflicting `--browser-path`/`--browser-version` may not be enforced. This can allow automation to proceed despite a mismatch, depending on cache state.The new major-version mismatch returns Err(...), but Selenium Manager may still exit successfully (code 0) via the cached-driver fallback path in main.rs. This undermines the intended "fail fast" behavior for conflicting --browser-path + requested version.
- The mismatch branch returns
Errwithout disabling fallback. -
main.rswillflush_and_exit(OK, ...)whenis_fallback_driver_from_cache()is true and a cached driver is found. -
fallback_driver_from_cachedefaults totrue, making this behavior likely on machines with any cached driver.
- rust/src/lib.rs[584-593]
Implementation direction:
- Before returning the mismatch
Err, callself.set_fallback_driver_from_cache(false);somain.rswon’t treat this user-input mismatch as recoverable. - Consider applying the same change to the specific-version mismatch error path as well (even though it’s outside this focus hunk) to keep behavior consistent.
[correctness] `--browser-path` major mismatch allowed
`--browser-path` major mismatch allowed
The new mismatch error path in `discover_local_browser()` only triggers when `is_browser_version_specific()` is true (i.e., the requested `--browser-version` contains a `.`), so a major-only version like `114`/`105` can still mismatch the detected version at an explicit `--browser-path` without failing fast. In that case, later major-version handling can set `download_browser=true` and effectively ignore the user-provided path, violating the requirement to clearly fail when `--browser-path` and `--browser-version` conflict.When --browser-path is provided together with a major-only --browser-version (e.g., 114/105), a mismatch between the detected browser version at that path and the requested major version does not trigger the new fail-fast mismatch error because it is gated by is_browser_version_specific() (versions containing .). This can allow later major-version mismatch handling to set download_browser = true and effectively ignore the explicit browser path, contrary to the requirement to clearly fail on --browser-path/--browser-version conflicts.
Compliance ID 1 requires a clear failure when --browser-path and --browser-version conflict.
The current mismatch error path only runs when is_browser_version_specific() is true (implemented as version.contains(".")), so major-only version requests bypass it.
Major-version mismatch handling happens later during major comparisons/reconciliation and can still set download_browser = true without checking whether original_browser_path / an explicit --browser-path was provided.
- rust/src/lib.rs[508-610]
- rust/src/lib.rs[855-867]
[correctness] Ignore breaks browser download
Ignore breaks browser download
If `--browser-version ignore` is used and no local browser is discovered, `setup()` proceeds to `download_browser()` with `original_browser_version="ignore"`, where it is treated as a non-empty non-stable version and its “major” parses to 0, triggering the minimum-version download guard and failing with a misleading error. This makes the newly introduced `ignore` escape hatch unreliable in environments where local discovery fails or no browser is installed.--browser-version ignore is introduced as a special value, but it is only handled in discover_local_browser() when a local browser version is successfully detected. If local discovery returns None, the code can still attempt a browser download with original_browser_version == "ignore", which then trips version parsing/min-version download logic and fails with an incorrect/misleading error.
-
setup()passesoriginal_browser_version(from config) intodownload_browser_if_necessary(). -
download_browser()usesget_major_browser_version()and parses it as an integer to enforce a minimum browser version for downloads; for"ignore"this effectively becomes0.
- rust/src/lib.rs[508-624]
- rust/src/lib.rs[238-266]
- rust/src/lib.rs[1400-1409]
Choose one (but ensure behavior is explicit and user-friendly):
-
Treat
ignoreas “unset” for download purposes: whenis_browser_version_ignore()is true, behave likebrowser_versionis empty/stable in download selection (e.g., request latest) OR clearoriginal_browser_versionbefore callingdownload_browser_if_necessary(). -
Validate and error early: if
browser-version==ignoreand there is no explicit browser path (and/or no local browser can be discovered), return a clear error explaining thatignoreis only meaningful when a local browser is available (especially when paired with--browser-path).
[correctness] Ignore skips WebView2 path
Ignore skips WebView2 path
When `--browser-version ignore` is used, `discover_local_browser()` sets the detected version and bypasses the existing WebView2 directory-to-`msedge(.exe)` path rewrite. This can leave `browser_path` pointing at a directory instead of the executable for WebView2 on Windows.The new is_browser_version_ignore() early branch sets browser_version and does not execute the later WebView2 path normalization block that converts a directory install path into the versioned msedge executable path.
- For WebView2,
get_browser_path_map()points to a directory (...\\EdgeWebView\\Application). - Existing code rewrites directory paths into
...\\{version}\\msedge(.exe)after version resolution. - The new
ignorebranch short-circuits before that rewrite.
- rust/src/lib.rs[528-610]
- rust/src/edge.rs[116-190]
[reliability] Ignore test missing exit-check
Ignore test missing exit-check
browser_version_ignore_no_detectable_browser_test() executes Selenium Manager and inspects output but never asserts the command fails with the expected exit code, so it can pass even if the CLI unexpectedly succeeds or exits with the wrong status. This weakens regression coverage for the newly-enabled `#[test]` path.browser_version_ignore_no_detectable_browser_test() runs the CLI and reads its captured stdout, but it never asserts the expected process outcome (failure + exit code). Since the product code returns an error when --browser-version ignore is used and no detectable local browser version is found, the test should explicitly assert .failure().code(DATAERR) to ensure the intended behavior is enforced.
In discover_local_browser() the ignore mode returns Err(anyhow!(...)) when version detection fails, and main.rs maps that to a DATAERR exit code (non-offline path). The test currently only checks for specific substrings in stdout.
- rust/tests/browser_tests.rs[186-216]
- rust/src/lib.rs[612-636]
- rust/src/main.rs[314-360]
[reliability] Temp fake browser collisions
Temp fake browser collisions
`create_fake_browser()` writes to a deterministic path under `std::env::temp_dir()` and never removes it, so parallel test execution or repeated runs can race/overwrite the same file and cause flaky CI failures. The two new tests also create the same fake version string, increasing the chance of collisions.The tests create an executable script at a deterministic temp path (based only on the version string) and do not clean it up. Rust tests can run in parallel, and both tests use the same version, so they can contend for the same file and introduce flaky behavior.
create_fake_browser("131.0.6778.264") is called by multiple tests, producing the same filename each time.
- rust/tests/browser_tests.rs[166-240]
- Use the
tempfilecrate (e.g.,tempfile::NamedTempFileor aTempDir) to generate a unique executable file per test. - Keep the temp handle alive for the duration of the test so the file isn’t deleted early.
- Optionally ensure cleanup (automatic with
tempfile) to avoid leaving artifacts in/tmp.
[maintainability] Ignore test misses success
Ignore test misses success
`browser_path_version_ignore_test()` never asserts the command succeeds (exit code 0), so it could pass even if Selenium Manager fails after emitting the expected warning text. This weakens regression coverage for the new `ignore` behavior.The browser_path_version_ignore_test inspects stdout but does not assert .success()/.code(0). A failing run could still emit the warning and satisfy the string checks, causing a false-positive test.
Other tests in this file explicitly assert success or expected failure codes.
- rust/tests/browser_tests.rs[212-240]
- Add
.success().code(0)(or.try_success()+assert_output) to the assertion chain before reading output. - Optionally assert the output contains the expected warning and that execution completes successfully.
| PR 17657 (2026-06-08) |
[reliability] Artifact outputs can collide
Artifact outputs can collide
generate_bidi_library hardcodes the shared artifact output filenames (bidi-ast.json / bidi-model.json) independent of the macro instance name. A second invocation of this macro in the same Bazel package would attempt to declare the same output paths and fail Bazel analysis due to output collisions.generate_bidi_library always emits bidi-ast.json and bidi-model.json as output filenames. If the macro is ever instantiated more than once in the same Bazel package, Bazel will report output path collisions.
This PR introduces shared artifacts via js_run_binary (*_ast and *_json targets). These outputs are currently fixed strings instead of being scoped by the macro instance name.
- javascript/selenium-webdriver/private/generate_bidi.bzl[157-191]
- Scope outputs by the macro instance name, e.g.:
-
ast_out = name + "-bidi-ast.json"(orname + "_ast.json") model_out = name + "-bidi-model.json"
-
- Keep the public artifact names stable if needed by adding a thin
filegroup(or alias target) that re-exports the uniquely named file under a stable label, rather than a stable filename.
| PR 17650 (2026-06-06) |
[reliability] Empty driver path crash
Empty driver path crash
delete-browsers-drivers.ps1 now builds $paths only from WebDriver env vars and calls Remove-Item unconditionally. If none of those env vars are set on the Windows runner, $paths is empty and PowerShell parameter binding fails before -ErrorAction applies, breaking the workflow step and leaving preinstalled drivers in place.Issue description
On Windows, Remove-Item -Path $paths ... is called even when $paths is empty (because it’s filtered from env vars only). PowerShell throws a parameter-binding error for an empty collection before -ErrorAction SilentlyContinue can suppress anything, causing the step to fail.
Issue Context
The workflow does not set these env vars explicitly for Windows; the step relies on the runner to provide them.
Fix Focus Areas
- scripts/github-actions/delete-browsers-drivers.ps1[12-20]
- .github/workflows/bazel.yml[202-208]
Suggested fix approach
- Add a guard:
- If
$paths.Count -eq 0, print a message andexit 0(or discover driver locations viaGet-Command chromedriver/msedgedriver/geckodriverand delete those paths).
- If
- Optionally: set these env vars in the workflow step (if known stable locations exist), or add fallback known locations used by GitHub Windows runners.
[maintainability] Misleading script filename
Misleading script filename
The Windows step and script comments now indicate only drivers are deleted, but the script filename remains delete-browsers-drivers.ps1. This mismatch can mislead future changes/reviews into assuming browsers are still deleted on Windows.The Windows deletion behavior was narrowed to drivers-only, but the script name still suggests it deletes browsers too.
This is a maintainability/clarity issue (not functional) that could cause confusion later.
- .github/workflows/bazel.yml[205-208]
- scripts/github-actions/delete-browsers-drivers.ps1[1-20]
- Rename the script to
delete-drivers.ps1(or similar) and update the workflow invocation accordingly; or - Keep the filename but add a short comment in the workflow step noting that the script is drivers-only on Windows.
This wiki is not where you want to be! Visit the Wiki Home for more useful links
Getting Involved
Triaging Issues
Releasing Selenium
Ruby Development
Python Bindings
Ruby Bindings
WebDriverJs
This content is being evaluated for where it belongs
Architectural Overview
Automation Atoms
HtmlUnitDriver
Lift Style API
LoadableComponent
Logging
PageFactory
RemoteWebDriver
Xpath In WebDriver
Moved to Official Documentation
Bot Style Tests
Buck
Continuous Integration
Crazy Fun Build
Design Patterns
Desired Capabilities
Developer Tips
Domain Driven Design
Firefox Driver
Firefox Driver Internals
Focus Stealing On Linux
Frequently Asked Questions
Google Summer Of Code
Grid Platforms
History
Internet Explorer Driver
InternetExplorerDriver Internals
Next Steps
PageObjects
RemoteWebDriverServer
Roadmap
Scaling WebDriver
SeIDE Release Notes
Selenium Emulation
Selenium Grid 4
Selenium Help
Shipping Selenium 3
The Team
TLC Meetings
Untrusted SSL Certificates
WebDriver For Mobile Browsers
Writing New Drivers