Skip to content

[rb] accept a whole-valued float for an integer BiDi field - #17939

Merged
titusfortner merged 1 commit into
SeleniumHQ:trunkfrom
titusfortner:rb-bidi-whole-float-integer
Aug 22, 2026
Merged

[rb] accept a whole-valued float for an integer BiDi field#17939
titusfortner merged 1 commit into
SeleniumHQ:trunkfrom
titusfortner:rb-bidi-whole-float-integer

Conversation

@titusfortner

Copy link
Copy Markdown
Member

🔗 Related Issues

Follows up on #17852, which added the integer/number primitive checks this loosens.

💥 What does this PR do?

An integer BiDi field now accepts a whole-valued float like 5.0 in addition to 5, inbound and outbound. A fractional value like 1.5 is still a mismatch and still raises.

🔧 Implementation Notes

  • PRIMITIVE_TYPES (a map of Ruby classes) becomes PRIMITIVE_CHECKS (a map of lambdas), so a schema primitive is defined by JSON kind rather than by Ruby class. JS has no int/float split, so a browser is free to spell an integer either way.
  • Inbound coerces a whole float to Integer so the parsed value matches the declared type; the conversion is exact. Outbound passes the value through unchanged.

🤖 AI assistance

  • AI assisted (complete below)
    • Tool(s): Claude Code (Opus 5)
    • What was generated: this description
    • I reviewed all AI output and can explain the change

🔄 Types of changes

  • Bug fix (backwards compatible)

@selenium-ci selenium-ci added C-rb Ruby Bindings B-devtools Includes everything BiDi or Chrome DevTools related labels Aug 22, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

BiDi: accept whole-valued floats for integer primitives

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Allow integer-typed BiDi fields to accept whole-valued floats (e.g., 5.0) on the wire.
• Reject fractional floats (e.g., 1.5) as true integer mismatches for inbound/outbound validation.
• Replace primitive Ruby-class matching with JSON-kind predicate checks for consistent typing.
Diagram

graph TD
  A["Caller code"] --> B["Record validation"] --> C["PRIMITIVE_CHECKS"] --> D["as_json payload"]
  E["Browser JSON"] --> F["Record read/from_json"] --> C --> G["Typed Ruby objects"]
  H["RSpec serialization_spec"] --> B

  subgraph Legend
    direction LR
    _usr["Caller/Test"] ~~~ _mod["Module/Logic"] ~~~ _data[("JSON payload")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Numeric + integerness check inline (no PRIMITIVE_CHECKS map)
  • ➕ Smaller refactor surface area (keep existing PRIMITIVE_TYPES for other primitives).
  • ➕ Less indirection for simple primitive cases.
  • ➖ Still needs special-casing for integer-vs-float acceptance in multiple call sites (scalar unions + primitive fields + outbound/inbound).
  • ➖ Harder to keep inbound/outbound behavior consistent as primitives expand.
2. Always coerce whole floats to Integer both inbound and outbound
  • ➕ Ensures outbound wire encoding is canonical (integers always emitted as integers).
  • ➕ Reduces downstream ambiguity for consumers that expect integer JSON tokens.
  • ➖ Potentially observable behavior change for callers that intentionally pass 5.0 and expect 5.0 to be emitted.
  • ➖ Not strictly necessary since the wire allows either spelling and outbound already originates from Ruby types.
3. Relax schema to treat integer as number everywhere
  • ➕ Eliminates integer/number split issues entirely across languages.
  • ➕ Simplifies validation logic.
  • ➖ Loses strictness: true integer-only fields would accept fractional values unless separately constrained.
  • ➖ Conflicts with the intent of the BiDi schema and prior primitive enforcement work.

Recommendation: The chosen approach (predicate-based PRIMITIVE_CHECKS + inbound coercion of whole floats to Integer) is the best balance: it keeps the schema’s integer semantics, matches JS/JSON realities, and centralizes the rule so scalar unions, inbound parsing, and outbound validation stay consistent. The new specs cover both the acceptance case (5.0) and the rejection case (5.5/1.5), reducing regression risk.

Files changed (2) +36 / -20

Bug fix (1) +24 / -18
record.rbAccept whole-valued floats for integer primitives via predicate checks +24/-18

Accept whole-valued floats for integer primitives via predicate checks

• Replaces PRIMITIVE_TYPES (Ruby class lists) with PRIMITIVE_CHECKS (predicate lambdas) to validate primitives by JSON kind. Updates scalar-union and outbound primitive validation to use these checks. Inbound reading now coerces whole-valued floats for integer fields into Integer while still rejecting fractional floats.

rb/lib/selenium/webdriver/bidi/serialization/record.rb

Tests (1) +12 / -2
serialization_spec.rbAdd coverage for whole-float integer acceptance and integer coercion +12/-2

Add coverage for whole-float integer acceptance and integer coercion

• Updates the outbound integer primitive test to reject fractional floats and adds a new test allowing whole-valued floats for integer fields. Adds an inbound test asserting whole-valued floats are accepted for integer-typed fields and coerced to Integer.

rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb

@qodo-code-review

qodo-code-review Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Whole-float misclassification ✗ Dismissed 🐞 Bug ≡ Correctness
Description
WHOLE_FLOAT uses Float modulo to decide integrality, which can treat very large fractional values as
“whole” after Float rounding, allowing a non-integer wire value to pass integer validation and be
coerced to Integer. This violates the intent that fractional values always raise and can silently
change values in extreme numeric ranges.
Code

rb/lib/selenium/webdriver/bidi/serialization/record.rb[R273-274]

+            WHOLE_FLOAT = ->(value) { value.is_a?(::Float) && value.finite? && (value % 1).zero? }
+            PRIMITIVE_CHECKS = {
Evidence
The PR introduces WHOLE_FLOAT’s modulo-based integrality test and then uses it to accept Float
values for the integer primitive; inbound, accepted floats are coerced to Integer via to_i.
Because Float rounding can erase fractional parts at large magnitudes, this pathway can accept and
coerce values that were not true integers on the wire.

rb/lib/selenium/webdriver/bidi/serialization/record.rb[269-279]
rb/lib/selenium/webdriver/bidi/serialization/record.rb[228-243]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WHOLE_FLOAT` currently checks integrality via `(value % 1).zero?`. For sufficiently large magnitudes, IEEE-754 Float cannot represent fractional increments (the parsed Float may already be rounded to an integer), so a wire value that was fractional can be accepted as an `integer` and then coerced via `to_i`.

### Issue Context
This PR intentionally accepts whole-valued floats for `integer` primitives and coerces inbound floats to `Integer`.

### Fix Focus Areas
- rb/lib/selenium/webdriver/bidi/serialization/record.rb[269-279]
- rb/lib/selenium/webdriver/bidi/serialization/record.rb[238-243]

### What to change
- Strengthen the definition of an “acceptable integer float” by additionally requiring it be within JS safe-integer bounds (since the sender is a browser JS Number) and/or explicitly documenting and enforcing the intended exactness constraint.
- For example:
 - Define a `SAFE_INTEGER = 9_007_199_254_740_991` constant.
 - Update `WHOLE_FLOAT` to require `value.abs <= SAFE_INTEGER` in addition to current checks.
 - Keep coercion (`to_i`) only after the strengthened check.

### Acceptance criteria
- A value like `5.0` is accepted.
- A value like `5.5` is rejected.
- A very large float with potential precision loss (e.g., magnitude > 2**53) is rejected for `integer` primitives when expressed as Float, preventing silent rounding/acceptance.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread rb/lib/selenium/webdriver/bidi/serialization/record.rb
@titusfortner
titusfortner merged commit 6df5706 into SeleniumHQ:trunk Aug 22, 2026
28 checks passed
This was referenced Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-devtools Includes everything BiDi or Chrome DevTools related C-rb Ruby Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants