Skip to content

.pr_agent_accepted_suggestions

qodo-merge-bot edited this page Jul 17, 2026 · 422 revisions
                     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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • .github/workflows/ci.yml[74-111]

Suggested fix

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.

Issue description

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.

Issue Context

  • These resources are produced by copy_file(...) outputs in py/BUILD.bazel.
  • Runtime Python code expects them to exist in the source tree when running from source (the purpose of py:local_dev).

Fix Focus Areas

  • 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.js
      • common/bidi-mutation-listener.js
      • firefox/webdriver_prefs.json
    • Optionally, in all mode, detect whether Dir.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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rake_tasks/python.rake[75-75]
    • Change the condition to arguments[:all] == 'all' (or explicitly validate accepted values and abort on unknown values).
  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rake_tasks/python.rake[77-93]
    • Add a guard that also detects untracked files under dest_dir (e.g., via SeleniumRake.git.status if available, or by invoking git status --porcelain --untracked-files=all -- <path> and checking for output).
    • Abort with a clear message listing the offending paths.

[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.

Issue description

A newly added comment restates the code behavior without explaining why the behavior is needed.

Issue Context

The comment sits directly above a checkout_file call whose intent is already clear from the method name and arguments.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

Spec links are optional; missing methods should ideally be reported by existing schema/model validators instead of causing an unhandled exception.

Fix Focus Areas

  • javascript/selenium-webdriver/project_bidi_schema.mjs[474-495]

Suggested change

  • Update link to 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 method to 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.

Issue description

extractAnchors() uses /id="([^"]+)"/g, which ignores id='...' (valid HTML). That can silently drop anchors and reduce specHref coverage.

Issue Context

This tool intentionally does a lightweight extraction (not a full DOM parse), but it should still handle common quoting forms in HTML output.

Fix Focus Areas

  • javascript/selenium-webdriver/extract_bidi_anchors.mjs[58-72]
  • javascript/selenium-webdriver/project_bidi_schema_test.mjs[485-513]

Suggested change

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • javascript/selenium-webdriver/extract_bidi_anchors.mjs[91-105]

Suggested change

  • 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.

Issue description

py/docs/README.rst still states that the docs use the sphinx-material theme, but the Sphinx config now uses pydata_sphinx_theme.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

The output directory path is wrapped in single backticks, which is interpreted-text markup in reStructuredText and renders differently than inline code/literal formatting.

Issue Context

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.

Fix Focus Areas

  • 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).

Issue description

SubscriptionScope introduces new public methods but they have no Javadoc. This violates the project requirement to document public API methods and include complete tags.

Issue Context

SubscriptionScope is public and annotated @Beta, so it is part of the user-visible Java API surface and should be documented.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/bidi/SubscriptionScope.java[33-54]

Suggested fix

  • Snapshot inputs in the setters, e.g. this.contexts = Set.copyOf(contexts); and this.userContexts = Set.copyOf(userContexts);.
  • (Optional) If you want to preserve insertion order for JSON output, consider copying to List.copyOf(...) in toMap() instead of returning a Set.

[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.

Issue description

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.

Issue Context

Other bindings implement session.subscribe with varying support for contexts and userContexts. The Java binding now exposes both via SubscriptionScope.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

Even though this API is @Beta, adding abstract methods to an existing public interface is a source/binary compatibility hazard for any external implementations.

Fix Focus Areas

  • 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.

Issue description

RemoteScript introduces @Deprecated annotations that omit forRemoval = true, which violates the deprecation compliance rule.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

The deprecated long-based removal flow deletes the legacy-id mapping before the actual unsubscribe happens, so failures leave no way to retry removal.

Issue Context

resolveLegacyId uses legacySubscriptionIds.remove(id) and is called before invoking biDi.removeListener(subscriptionId).

Fix Focus Areas

  • java/src/org/openqa/selenium/remote/RemoteScript.java[77-91]
  • java/src/org/openqa/selenium/remote/RemoteScript.java[166-178]

Suggested direction

Change the flow to:

  1. look up subscription id without removing it (e.g., get),
  2. attempt biDi.removeListener(subscriptionId),
  3. 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.

Issue description

subscribe(...) returns (String) result.get("subscription") without checking for null/type/blank.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/bidi/BiDi.java[122-125]
  • java/src/org/openqa/selenium/bidi/Connection.java[178-187]

Suggested direction

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.

Issue description

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.

Issue Context

  • BiDi.addListener sends session.subscribe on every subscription.
  • BiDi.removeListener only removes local callbacks.
  • BiDi.clearListener(event) only sends session.unsubscribe when Connection.isEventSubscribed(event) is true, but Connection.removeListener removes the event entry when the last local listener is removed.

Fix Focus Areas

  • 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]

Suggested direction

  • Introduce a subscription type that retains enough info to unsubscribe on the wire (e.g., Subscription holding Event, optional context IDs, and local listener id), and expose unsubscribe(Subscription) / Subscription.close().
  • Alternatively, teach Connection.removeListener/BiDi.removeListener to perform a session.unsubscribe when the last local handler for an event is removed (requires tracking whether the subscription was global vs context-scoped, leveraging contextListenerIds in BiDi).

[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.

Issue description

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.

Issue Context

HasBiDi is annotated @Beta and now exposes Handle directly. Module and BiDi are also @Beta, but Handle itself is not.

Fix Focus Areas

  • java/src/org/openqa/selenium/bidi/Handle.java[18-31]

Suggested fix

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/*`.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • AGENTS.md[25-30]

Suggested change

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).

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/remote/RemoteWebDriver.java[291-296]
  • java/src/org/openqa/selenium/remote/RemoteWebDriver.java[436-458]

Suggested change

  • Make createBiDi() non-throwing for missing/invalid webSocketUrl and return Optional.empty() (optionally log at WARNING/FINE).
  • If you still want an explicit failure, move it to getBiDi() (or override getBiDi() in RemoteWebDriver) so session creation succeeds but BiDi access fails with a clear message when BiDi is requested but unavailable.
  • Consider tracking a bidiRequested flag (based on the requested capabilities) to produce an accurate exception message when getBiDi() 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/remote/RemoteWebDriver.java[436-440]

Suggested implementation sketch:

  • If rawUrl is a String, trim it.
  • Parse it with new URI(trimmed).
  • Validate uri.getScheme() via equalsIgnoreCase("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.

Issue description

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.

Issue Context

  • org.openqa.selenium.bidi.Connection opens the websocket in its constructor.
  • JdkHttpClient.openSocket throws ConnectionFailedException on connection/setup failures.

Fix Focus Areas

  • java/src/org/openqa/selenium/remote/RemoteWebDriver.java[435-459]

Implementation sketch

  • Wrap the Connection/BiDi construction in a try { ... } 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, close wsClient, and return Optional.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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/remote/RemoteWebDriver.java[451-454]

Suggested fix

  • Remove the + webSocketUrl concatenation from the WARNING log, or log a redacted/sanitized version (e.g., scheme+host+port only; strip path/query), and optionally log the exception via LOG.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.

Issue description

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.

Issue Context

This module is shared by pinned_browsers and update_cdp, so an unclear failure here blocks scheduled pinning and release workflows.

Fix Focus Areas

  • scripts/chrome_version.py[18-25]
  • scripts/chrome_version.py[28-35]
  • scripts/chrome_version.py[38-52]

What to change

  • Check r.status for each http.request(...) and raise ValueError with the URL and status when non-200.
  • In chrome_for_milestone, materialize the filtered list and raise a ValueError like No Chrome-for-Testing versions found for milestone X rather 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • scripts/pinned_browsers.py[30-39]
  • scripts/update_cdp.py[17-26]

Implementation notes

  • After each http.request(...), check resp.status != 200 and raise a ValueError that includes HTTP status and URL (similar to calculate_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".

Issue description

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.

Issue Context

This is a CLI-exposed footgun: the error is not actionable and will look like an internal failure instead of invalid input.

Fix Focus Areas

  • scripts/chrome_version.py[14-27]

Suggested fix approach

  • 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 ValueError with 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.

Issue description

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.

Issue Context

The repo already has a similar streaming download pattern that explicitly releases the connection.

Fix Focus Areas

  • scripts/pinned_browsers.py[20-28]

What to change

  • Wrap the streaming read in try/finally and call r.release_conn() in the finally block.
  • 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.

Issue description

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.

Issue Context

update_module() already uses re.subn() and validates count == 1, showing the intended robustness pattern.

Fix Focus Areas

  • scripts/update_cddl.py[100-111]
  • scripts/update_cddl.py[113-125]

Implementation notes

  • Replace the two re.sub(...) calls with re.subn(...) and assert count == 1 for each substitution.
  • If either count is not 1, raise a RuntimeError that 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 in common/webref_cddl.bzl to 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/json/JsonInput.java[223-281]
  • java/src/org/openqa/selenium/json/Input.java[31-38]

Suggested fix

  • Add a small helper (e.g., describeChar(int c)) that returns "<EOF>" when c == Input.EOF, otherwise returns a quoted printable character.
  • Use that helper in the three JsonException messages added in nextNumber() (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]`.

Issue description

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.

Issue Context

  • markElementRead() sets the current container flag to true and never sets it back to false.
  • hasNext() consumes the comma but does not update containerHasElement, so the subsequent non-comma path treats the next value token as a missing-comma violation.

Fix Focus Areas

  • 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]

How to fix

  1. Treat the per-container boolean as “needsSeparator” (or add a separate boolean/stack for that state).
  2. In hasNext() when a comma is successfully consumed, clear the flag for the current container (set it to false) so repeated hasNext() calls remain valid until an element is actually read.
    • e.g., add a helper like markSeparatorRead() that sets the top boolean to false.
  3. Add a regression test demonstrating idempotency:
    • Parse [1,2], read 1, call hasNext() twice (both should be true), then read 2.
    • Same idea for objects: {"a":1,"b":2} after reading a:1, call hasNext() twice before reading b.
  4. 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/json/JsonInput.java[403-441]

Suggested fix

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.

Issue description

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}).

Issue Context

  • 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 calling hasNext().

Fix Focus Areas

  • java/src/org/openqa/selenium/json/JsonInput.java[318-346]
  • java/src/org/openqa/selenium/json/JsonInput.java[534-574]

Suggested fix approach

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:

  1. If the current container has already seen an element/pair, require a comma before the next element/pair.
  2. Consume exactly one comma (if present) and then continue.
  3. 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.

Issue description

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).

Issue Context

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.

Fix Focus Areas

  • 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]

Suggested fix

Add tests similar to the array ones, e.g.:

  • Reject missing comma between object entries: {"a":1 "b":2} should throw from hasNext() after reading the first value.
  • Reject leading comma in object: {,"a":1} should throw from hasNext() immediately after beginObject().


                     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.

Issue description

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”.

Issue Context

  • The Bazel target for these tests does not specify -encoding in javacopts, so source encoding is not explicitly pinned at the target level.

Fix Focus Areas

  • Replace the literal occurrences with a portable Java escape (e.g. "\"a\uFFFFb\"" and expected "a\uFFFFb"), or build the string with new 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rb/lib/selenium/webdriver/bidi/serialization/record.rb[92-105]
  • rb/lib/selenium/webdriver/bidi/serialization/record.rb[118-125]

Suggested fix

In read(field, raw), when raw.nil?:

  • return nil only if field.nullable is 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.

Issue description

The YARD doc block immediately above module Serialization does not include # @api private.

Issue Context

This module is part of the internal BiDi generated-protocol runtime and should be explicitly marked private per compliance requirements.

Fix Focus Areas

  • 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.

Issue description

BiDiGenerate is an internal code generator module but its YARD doc block is missing # @api private directly above module BiDiGenerate.

Issue Context

Compliance requires internal Ruby APIs to be marked with @api private in YARD docs so they are not treated as supported public surface area.

Fix Focus Areas

  • 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.

Issue description

RSpec mocking (instance_double, allow(...).to receive) is used in new BiDi unit tests, violating the "avoid mocks" compliance requirement.

Issue Context

The compliance rule allows simple in-memory fakes that implement the same interface without a mocking framework, or contract-driven stubs/integration tests.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rb/lib/selenium/webdriver/bidi/serialization/record.rb[164-177]
  • rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb[304-313]

Implementation notes

  • Update integer validation to reject non-integral numerics. Options:
    • Strict: accept only Integer.
    • Lenient but correct: accept Integer and Float only when raw.finite? && raw % 1 == 0.
  • Update/replace the spec that currently asserts 1.5 is accepted for an integer field to instead expect an error (and optionally add a case that accepts 1.0 if 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.

Issue description

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.

Issue Context

  • The schema artifact //javascript/selenium-webdriver:create-bidi-src_schema is produced as a generated output via js_run_binary.
  • The verifier check_generated.rb reads the schema using File.read(schema_path) after a minimal Dir.pwd-based fallback.
  • Ruby CI now runs bazel test ... //rb/..., so the new verify test will run in CI.

Fix Focus Areas

  • rb/lib/selenium/webdriver/BUILD.bazel[47-61]
  • rb/lib/selenium/webdriver/bidi/support/check_generated.rb[27-31]

Suggested fix

  1. 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).
  2. 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) when File.exist?(schema_path) is false.
  3. 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.

Issue description

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).

Issue Context

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.

Fix Focus Areas

  • Change the Hash serialization filter to drop only Serialization::UNSET, not nil.

  • Update any affected specs/callers to pass Serialization::UNSET when 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).

Issue description

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).

Issue Context

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.

Fix Focus Areas

  • Implement a non-raising inbound behavior for unmatched unions (e.g., return the raw json_payload hash for unknown cases, or introduce a generic Unknown wrapper type) while keeping build strict for outbound.

  • Consider rescuing ArgumentError/NameError in from_json and returning json_payload when 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.

Issue description

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.

Issue Context

BiDi owns the websocket connection in an instance variable and does not expose it as part of its API.

Fix Focus Areas

  • rb/lib/selenium/webdriver/remote/bidi_bridge.rb[20-34]
  • rb/lib/selenium/webdriver/bidi.rb[34-64]

Suggested fix

Introduce an explicit API seam instead of instance_variable_get, e.g.:

  • Add a private BiDi#connection reader (or BiDi#transport) returning the underlying WebSocketConnection (or a BiDi::Transport) and use that from BiDiBridge.
  • Alternatively, make BiDi::Transport accept a BiDi instance and adapt to its public send_cmd API.

[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.

Issue description

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.

Issue Context

Transport#execute forwards method: and serialized params: to send_cmd, so the spec should assert the outgoing params include the validated value.

Fix Focus Areas

  • rb/spec/unit/selenium/webdriver/bidi/protocol_spec.rb[51-58]

Suggested change

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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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]

Suggested fix

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, after super, attempt to compute socket_url = validated_socket_url.
  • If validation fails, call execute(:delete_session) (guarded by presence of a session id) and http.close (rescuing the same QUIT_ERRORS patterns), then re-raise. Optionally also harden BiDiBridge#quit to handle @bidi being 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.

Issue description

New/modified public API methods must be documented with YARD tags.

Issue Context

Safari::Options#enable_bidi! was added and changes Safari capability behavior, but it has no doc block.

Fix Focus Areas

  • rb/lib/selenium/webdriver/safari/options.rb[48-51]

Suggested approach

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.

Issue description

The newly added test function docstring is not Google-style and does not document its parameters under an Args: section.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

The compliance requirement explicitly calls out eliminating EnvironmentManager-style static/shared context to enable safe parallel test execution.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/src/org/openqa/selenium/docker/Version.java[91-115]
  • java/src/org/openqa/selenium/docker/VersionCommand.java[86-113]

Suggested fix

In compare() when both segments are numeric:

  • Either wrap parseLong in a try/catch and on NumberFormatException fall back to a safe numeric comparison that doesn’t overflow (e.g., compare by string length, then lexicographically), or
  • Parse using BigInteger for 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.

Issue description

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.

Issue Context

Compliance prefers comments/documentation to explain intent/rationale rather than restating obvious mechanics or hiding required fields in HTML comments.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

This is in the Out of scope section under "Partial BiDi implementation support".

Fix Focus Areas

  • docs/plans/selenium-5.md[47-55]

Suggested fix

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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • java/test/org/openqa/selenium/internal/EitherTest.java[22-40]
  • .bazelrc[23-35]

Suggested fix

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.

Issue description

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.

Issue Context

Compliance requires that any changed public API method in non-test code has complete Javadoc including a descriptive sentence and correct tags.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • dotnet/src/webdriver/BiDi/EventStream.cs[64-105]

Suggested fix approach

  • Return a custom IAsyncEnumerable<TEventArgs> implementation (not an async-iterator directly) whose GetAsyncEnumerator(...):
    • Uses an Interlocked.CompareExchange guard to allow only a single enumerator to be created.
    • Creates any linked cancellation token source inside GetAsyncEnumerator and ensures it is disposed when the enumerator is disposed.
  • Alternatively (if intended), change semantics to allow multiple enumerations by removing SingleReader = true and 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.

Issue description

Public BiDi streaming APIs were changed incompatibly (IEventStream<TEventArgs> -> IAsyncEnumerable<TEventArgs>), and the old surface is not preserved with a deprecation phase.

Issue Context

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.

Fix Focus Areas

  • dotnet/src/webdriver/BiDi/IBiDi.cs[68-70]
  • dotnet/src/webdriver/BiDi/BiDi.cs[120-130]

Implementation guidance (high level)

  • Reintroduce a compatibility surface for existing callers, e.g. keep StreamAsync returning Task<IEventStream<TEventArgs>> (or add a differently-named method for the new IAsyncEnumerable<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.

Issue description

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.

Issue Context

  • EventDispatcher.SubscribeReaderAsync eagerly subscribes and adds the EventStream to slots.
  • EventStream only unsubscribes in DisposeAsync, which is no longer reachable via the public return type.

Fix Focus Areas

  • 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 EventStream if enumeration starts, and does nothing if it never starts.

Suggested approach

  • Change BiDi.StreamAsync(...) (and EventSource/ContextEventSource.StreamAsync) to return an IAsyncEnumerable<T> that performs the SubscribeReaderAsync(...) call inside the enumerator (e.g., lazy-init on first MoveNextAsync).
  • Keep EventDispatcher.SubscribeReaderAsync and EventStream mostly as-is; the key is to avoid calling them before enumeration.

Fix Focus Areas (files/lines)

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • dotnet/src/webdriver/BiDi/EventStream.cs[73-104]

Suggested fix approach

  • Defer CancellationTokenSource.CreateLinkedTokenSource(...) until enumeration actually begins (e.g., inside GetAsyncEnumerator of 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.

Issue description

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.

Issue Context

  • UnsubscribingAsyncEnumerator.DisposeAsync() calls _owner.DisposeAsync().
  • EventStream.DisposeAsync() completes the channel.
  • GetAsyncEnumerator() does not check _disposed or enforce single enumeration.

Fix Focus Areas

  • Fail fast on misuse (e.g., throw ObjectDisposedException if _disposed != 0).
  • Optionally enforce "single active enumerator" (since the channel is configured SingleReader = true), e.g., track an _enumeratorCreated flag and throw if called twice.

Fix Focus Areas (files/lines)

  • 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.

Issue description

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.

Issue Context

The current control flow creates defs before validating that all branches are supported.

Fix Focus Areas

  • javascript/selenium-webdriver/normalize_bidi_ast.mjs[327-367]

Suggested fix

Refactor to be atomic per def:

  1. First, pre-validate and compute all branchType() entries for detected.branches.
  2. If any entry is null/unsupported, do not create any synthesized defs and do not modify def.
  3. Otherwise, build synthesized defs into a temporary local array (e.g., newDefs) and only then append them to created and rewrite def to Type='variable'.
  4. 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.

Issue description

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.

Issue Context

  • PRELUDE.nil is mapped to 'null', and projectEntry() can correctly project a nil group ref to { primitive: 'null' }.
  • But isNullAlt() strips it before projection, which is only correct when there is at least one non-null alternative.

Fix Focus Areas

  • javascript/selenium-webdriver/project_bidi_schema.mjs[73-87]

Suggested fix

  • In projectRef(type), after computing entries, add a guard:
    • If entries.length === 0 and all.length > 0, return { primitive: 'null' } (and do not set nullable), because the type is exactly null.
    • Only set node.nullable = true when there is at least one non-null alternative (i.e., entries.length > 0 && entries.length < all.length).
  • Optionally add/extend a unit test to cover nil-only and null-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.

Issue description

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.

Issue Context

Aliases are produced by projectType() for variable defs that are not pure enums and not unions of refs.

Fix Focus Areas

  • javascript/selenium-webdriver/project_bidi_schema.mjs[106-116]
  • javascript/selenium-webdriver/project_bidi_schema.mjs[166-200]

Proposed fix

  • In checkSchema(), add a branch for node.kind === 'alias' that runs refsIn(node.type) and errors on any ref that does not exist in schema.types.
  • Ensure refsIn can traverse the alias RHS as-is (it already handles ref/list/map/union/record type-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 the node.kind === 'record' branch, also validate node.map if present:
    • for (const r of refsIn(node.map)) if (!has(r)) errors.push(${name}: unresolved map value type ${r})
  • Keep existing fields validation 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • javascript/selenium-webdriver/normalize_bidi_ast.mjs[332-365]

Suggested fix approach

  1. 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.
  2. If any branch is unsupported, bail out without touching the allocator.
  3. Only after confirming all branches are supported, loop over the staged branch infos and call alloc(...) to assign final synthNames and build stagedDefs / 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 cddl2ts says a field is nullable and the schema is not
  • it does not error when the schema marks a field nullable but cddl2ts does 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?.nullable then 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.

Issue description

A new internal helper method (assert_local_arguments) is publicly exposed and lacks a # @api private YARD tag.

Issue Context

This helper is used only to validate constructor arguments for local drivers and is not intended as a supported API.

Fix Focus Areas

  • 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.

Issue description

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).

Issue Context

The compliance checklist requires all public API methods in changed code to be documented with YARD tags and a short description.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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):

  1. Docs-only: change YARD to only accept Proxy (keep current behavior).
  2. Support Hash: accept Hash by converting it to a Proxy during initialization and/or assignment (e.g., in initialize or proxy=), and update the YARD (and any related type definitions such as RBS) accordingly.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rb/lib/selenium/webdriver/remote/http/default.rb[126-131]

Implementation notes

  • In follow_redirect, use common_headers.dup instead of DEFAULT_HEADERS.dup when calling request.
  • 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.

Issue description

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.

Issue Context

This repo disables blank issues, so users are expected to create issues via templates.

Fix Focus Areas

  • docs/decisions/README.md[44-48]

Suggested fix

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.

Issue description

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.

Issue Context

This is contributor-facing guidance. Aligning the PR template with the README process reduces confusion and avoids back-and-forth during PR creation.

Fix Focus Areas

  • .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.

Issue description

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.

Issue Context

CI runs ./go format, which applies google-java-format to all java/**.java files, and then fails if git diff is non-empty.

Fix

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).

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

The repo already documents output_base correctly as a startup option in README.md, and .bazelrc uses startup stanzas for startup-only flags.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

Repo documentation says bindings track convergence in the decision’s binding-status table, and the template provides the exact section/table to include.

Fix Focus Areas

  • docs/decisions/17670-bidi-is-an-implementation-mechanism.md[17-23]
  • docs/decisions/17670-bidi-is-an-implementation-mechanism.md[81-87]

Suggested change

  1. Add a ## Binding status section at the end, using the template’s table columns (Binding | Status | Notes / tracking link).
  2. Optionally repurpose the existing “Current behavior” details into the Notes column.
  3. 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

The ADR header is missing the - Date: YYYY-MM-DD field required by the ADR template.

Issue Context

The template specifies that the header must include a date indicating when the status last changed.

Fix Focus Areas

  • docs/decisions/17670-bidi-is-an-implementation-mechanism.md[3-5]

Suggested change

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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • docs/decisions/0001-bidi-is-an-implementation-mechanism.md[38-40]

Suggested fix

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • docs/decisions/README.md[1-45]
  • docs/decisions/.index.md[1-3]

Suggested fix

Do one of the following:

  1. Add a prominent link to .index.md near the top of README.md (e.g., “Index of Decisions”).
  2. Rename .index.md to index.md or INDEX.md and link to it from README.md.
  3. Alternatively, merge the index list into README.md and 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.

Issue description

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.

Issue Context

This is a documentation consistency/traceability problem inside the ADR itself.

Fix Focus Areas

  • docs/decisions/0001-bidi-is-an-implementation-mechanism.md[44-56]

Expected change

Either:

  1. Populate the “Notes / tracking link” cells with the relevant tracking issues/PRs per binding, or
  2. 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.

Issue description

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).

Issue Context

The tar extraction path already explicitly rejects an empty entry path; pkg/cpio should match that safety behavior.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

unzip() already has logic to strip the first component only when the archive paths actually contain a parent folder; tar extraction should behave similarly.

Fix Focus Areas

  • rust/src/files.rs[375-390]
  • rust/src/files.rs[419-432]

Suggested fix

  • 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_path for regular-file entries to avoid writing directly to target.

[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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rust/src/files.rs[149-167]
  • rust/src/files.rs[360-375]

Suggested fix approach

  • Refactor untargz() to iterate archive.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_path after validation, or using an unpack-into-directory API if available).
  • Optionally add a unit test similar to uncompress_tar_rejects_path_traversal_entry, but covering the .gz path 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • rust/src/files.rs[95-103]
  • rust/src/files.rs[219-237]

Suggested fix

  • Update check_path_traversal() to reject any non-relative components, not just ParentDir, e.g. fail on Component::RootDir and Component::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 under target.
  • Add unit/integration tests (if available in this repo) covering entry names like /tmp/evil, \\server\share\evil (Windows), and C:\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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

  • The mismatch branch returns Err without disabling fallback.
  • main.rs will flush_and_exit(OK, ...) when is_fallback_driver_from_cache() is true and a cached driver is found.
  • fallback_driver_from_cache defaults to true, making this behavior likely on machines with any cached driver.

Fix Focus Areas

  • rust/src/lib.rs[584-593]

Implementation direction:

  • Before returning the mismatch Err, call self.set_fallback_driver_from_cache(false); so main.rs won’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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

--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.

Issue Context

  • setup() passes original_browser_version (from config) into download_browser_if_necessary().
  • download_browser() uses get_major_browser_version() and parses it as an integer to enforce a minimum browser version for downloads; for "ignore" this effectively becomes 0.

Fix Focus Areas

  • rust/src/lib.rs[508-624]
  • rust/src/lib.rs[238-266]
  • rust/src/lib.rs[1400-1409]

Suggested implementation direction

Choose one (but ensure behavior is explicit and user-friendly):

  1. Treat ignore as “unset” for download purposes: when is_browser_version_ignore() is true, behave like browser_version is empty/stable in download selection (e.g., request latest) OR clear original_browser_version before calling download_browser_if_necessary().
  2. Validate and error early: if browser-version==ignore and there is no explicit browser path (and/or no local browser can be discovered), return a clear error explaining that ignore is 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.

Issue description

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.

Issue Context

  • 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 ignore branch short-circuits before that rewrite.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • 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.

Issue description

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.

Issue Context

create_fake_browser("131.0.6778.264") is called by multiple tests, producing the same filename each time.

Fix Focus Areas

  • rust/tests/browser_tests.rs[166-240]

Suggested implementation direction

  • Use the tempfile crate (e.g., tempfile::NamedTempFile or a TempDir) 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.

Issue description

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.

Issue Context

Other tests in this file explicitly assert success or expected failure codes.

Fix Focus Areas

  • rust/tests/browser_tests.rs[212-240]

Suggested implementation direction

  • 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.

Issue description

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.

Issue Context

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.

Fix Focus Areas

  • javascript/selenium-webdriver/private/generate_bidi.bzl[157-191]

Suggested fix

  • Scope outputs by the macro instance name, e.g.:
    • ast_out = name + "-bidi-ast.json" (or name + "_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 and exit 0 (or discover driver locations via Get-Command chromedriver/msedgedriver/geckodriver and delete those paths).
  • 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.

Issue description

The Windows deletion behavior was narrowed to drivers-only, but the script name still suggests it deletes browsers too.

Issue Context

This is a maintainability/clarity issue (not functional) that could cause confusion later.

Fix Focus Areas

  • .github/workflows/bazel.yml[205-208]
  • scripts/github-actions/delete-browsers-drivers.ps1[1-20]

Suggested fix approach

  • 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.


Clone this wiki locally