Skip to content

node:assert: preserve partial semantics for TypedArrays, Sets and sparse arrays in partialDeepStrictEqual - #34585

Open
robobun wants to merge 6 commits into
mainfrom
farm/4e6809c4/partial-deep-strict-equal
Open

node:assert: preserve partial semantics for TypedArrays, Sets and sparse arrays in partialDeepStrictEqual#34585
robobun wants to merge 6 commits into
mainfrom
farm/4e6809c4/partial-deep-strict-equal

Conversation

@robobun

@robobun robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

assert.partialDeepStrictEqual lost its partial semantics in three branches of compareBranch(), producing a false AssertionError on Bun for inputs that pass on Node v26.3.0:

import assert from "node:assert";
const pdse = assert.partialDeepStrictEqual;

// (a) binary: Node matches expected as an in-order byte subsequence of actual
pdse(new Uint8Array([1, 2, 3]), new Uint8Array([1, 3]));   // node: ok, bun: AssertionError
pdse(Buffer.from([1, 2, 3]), Buffer.from([1, 2]));         // node: ok, bun: AssertionError

// (b) Set members: Node compares each member with the partial algorithm
pdse(new Set([{ a: 1, b: 2 }]), new Set([{ a: 1 }]));      // node: ok, bun: AssertionError

// (c) holes in expected are skipped by Node, read as undefined by Bun
pdse([1, 2, 3], [, 2]);                                    // node: ok, bun: AssertionError

Cause

Three separate spots in compareBranch() (src/js/node/assert.ts):

  • The ArrayBufferView / isAnyArrayBuffer branch bailed to Bun.deepEquals(actual, expected, true), which requires full byte equality. Node's partial mode matches the expected bytes as an in-order subsequence of the actual bytes (isPartialArrayBufferView / isPartialUint8Array in lib/internal/util/comparisons.js), after gating on the %TypedArray% @@toStringTag so cross-kind pairs (e.g. Uint8Array vs Int8Array) still reject.
  • The Set branch compared each expected member against actual members with isDeepStrictEqual (full strict equality), so {a:1} never matched {a:1,b:2}. Node recurses with the partial algorithm per member (setEquiv / partialObjectSetEquiv). The branch also skipped the existing cycle guard.
  • compareBranchArray indexed expected[i] with no own-property check, so a hole became a required undefined. Node skips holes on both sides, matching the own-index values of expected as an in-order subsequence of the own-index values of actual (partialArrayEquiv / partialSparseArrayEquiv), with the expected.length > actual.length gate preserved.

Fix

Replace each of the three branches with Node's algorithm: a byte-wise subsequence matcher for views and buffers (isPartialUint8Array), a recursive compareBranch call for Set members inside the existing withCycleGuard, and a dense-then-sparse subsequence scan for arrays that consults hasOwnProperty before reading an index. Buffer.isBuffer is dropped from typesToCallDeepStrictEqualWith since a Buffer is an ArrayBufferView and is now handled by the view branch.

Verification

  • The four new tests in test/js/node/assert/assert.test.cjs cover all three families plus the rejecting edges (out-of-order bytes, longer expected, cross-kind views, ArrayBuffer vs SharedArrayBuffer, expected member with extra keys, holes exceeding actual.length, explicit undefined vs a hole in actual). Every assertion was checked against Node v26.3.0.
  • bun bd test test/js/node/assert/ passes (355 tests), as do test-assert-typedarray-deepequal.js and test-assert.js.
  • Fail-before: the four new tests fail with src/ stashed and pass with the fix applied.

#33068 replaces the whole comparison path with a port of Node's internal/util/comparisons, which would also fix these; this PR is the targeted fix for the three reported branches in the current implementation.


[review] gate passed · iteration 2 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/assert/assert-partial-deep-strict-equal.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (07ff6ff66)

test/js/node/assert/assert-partial-deep-strict-equal.test.ts:
1 | import { describe, expect, test } from "bun:test";
2 | import assert from "node:assert";
3 | 
4 | describe("assert.partialDeepStrictEqual", () => {
5 |   test("TypedArrays, Buffers and DataViews match the expected bytes as an in-order subsequence", () => {
6 |     assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 3]));
               ^
AssertionError: Uint8Array(3) [
  1,
  2,
  3
] partialDeepStrictEqual Uint8Array(2) [
  1,
  3
]
      at innerFail (node:assert:62:32)
      at partialDeepStrictEqual (node:assert:342:14)
      at <anonymous> (/workspace/bun/test/js/node/assert/assert-partial-deep-strict
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (1e7b63531)

test/js/node/assert/assert-partial-deep-strict-equal.test.ts:
(pass) assert.partialDeepStrictEqual > TypedArrays, Buffers and DataViews match the expected bytes as an in-order subsequence [4.53ms]
(pass) assert.partialDeepStrictEqual > ArrayBuffers match the expected bytes as an in-order subsequence [0.48ms]
(pass) assert.partialDeepStrictEqual > Set members are compared with the partial algorithm [1.25ms]
(pass) assert.partialDeepStrictEqual > holes in the expected array are skipped [0.58ms]

 4 pass
 0 fail
 12 expect() calls
Ran 4 tests across 1 file. [485.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/assert/assert-partial-deep-strict-equal.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (07ff6ff66)

test/js/node/assert/assert-partial-deep-strict-equal.test.ts:
(pass) assert.partialDeepStrictEqual > TypedArrays, Buffers and DataViews match the expected bytes as an in-order subsequence [311.92ms]
(pass) assert.partialDeepStrictEqual > ArrayBuffers match the expected bytes as an in-order subsequence [54.11ms]
(pass) assert.partialDeepStrictEqual > Set members are compared with the partial algorithm [113.00ms]
(pass) assert.partialDeepStrictEqual > holes in the expected array are skipped [72.75ms]

 4 pass
 0 fail
 12 expect() calls
Ran 4 tests across 1 file. [5.25s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 1762ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/20] gen JS modules (bundle-modules)
Preprocess modules (14333ms)
Bundle modules (83ms)
Postprocesss modules (160ms)
Bundle Functions (1218ms)
Generate Code (161ms)

[16.01s] Bundled "src/js" for production
  2038 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[1/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current ve
... (truncated)
diff hotspot
src/js/node/assert.ts                              | 120 ++++++++++++++++-----
 .../assert-partial-deep-strict-equal.test.ts       |  85 +++++++++++++++
 2 files changed, 176 insertions(+), 29 deletions(-)

gate history · 1 passed · 2 rejected · iteration 2

evidence per changed file
file                                                      reads  edits  tests
src/js/node/assert.ts                                         4      9      0
…js/node/assert/assert-partial-deep-strict-equal.test.ts      0      1      0

…rse arrays in partialDeepStrictEqual

Three branches in compareBranch() were falling through to full strict
equality instead of Node's partial-subset semantics:

- ArrayBufferView/ArrayBuffer operands delegated to Bun.deepEquals, so a
  shorter expected view never matched. Node compares the expected bytes
  as an in-order subsequence of the actual bytes after a
  Symbol.toStringTag gate (isPartialArrayBufferView / isPartialUint8Array).
- Set members were compared with isDeepStrictEqual, so an expected
  member with fewer keys never matched. Node recurses with the partial
  algorithm per member and claims distinct actual members greedily.
- Holes in the expected array were read as undefined via expected[i],
  so a sparse expected forced an undefined match. Node skips holes on
  both sides (partialArrayEquiv / partialSparseArrayEquiv).

Each of these surfaces as a false AssertionError on Bun for inputs that
pass on Node v26, which blocks test suites porting from Node that use
the partial matcher on binary data, Sets of objects, or sparse expected
arrays.

All expectations in the added tests were verified against Node v26.3.0.
@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:48 AM PT - Jul 18th, 2026

@robobun, your commit 07ff6ff has 3 failures in Build #75248 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34585

That installs a local version of the PR into your bun-34585 executable, so you can run:

bun-34585 --bun

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36492d1a-dfb2-401c-9744-6ba615eb5989

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf2d50 and 07ff6ff.

📒 Files selected for processing (2)
  • src/js/node/assert.ts
  • test/js/node/assert/assert-partial-deep-strict-equal.test.ts

Comment @coderabbitai help to get the list of available commands.

Comment thread src/js/node/assert.ts Outdated
…dial

The captured TypedArrayPrototypeGetByteLength was applied only to
freshly-constructed Uint8Array values inside isPartialUint8Array, after
Uint8ArrayFromBuffer already read .buffer/.byteOffset/.byteLength from
the user-supplied view via ordinary prototype lookups, so the capture
added nothing. Drop it and read .byteLength directly.

For the ArrayBuffer vs SharedArrayBuffer gate, compare
isSharedArrayBuffer(actual) against isSharedArrayBuffer(expected), which
is a brand check, instead of reading Symbol.toStringTag off the value.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline nit, I traced the startA/startB handoff into compareBranchSparseArray — the dense-prefix invariant (every actual[0..startA-1] and expected[0..startB-1] is verified non-hole before switching) means array positions and ObjectKeys positions coincide at the switchover, so keysA[startA] / keysB[startB] index correctly. Also checked the moved bounds check in the dense while-loop against the old lastCandidate form — equivalent, and the entry invariant actualPos ≤ actual.length - expected.length + i holds at each iteration start.

Extended reasoning...

The sparse-array handoff was the non-obvious part of this diff: compareBranchArray passes dense array positions (actualPos, i) as indices into ObjectKeys(actual) / ObjectKeys(expected). That only works if the arrays are dense up to those positions so that position-in-array == position-in-keys-list. Traced both entry paths (hole in expected[i], hole in actual[actualPos]) and confirmed every prior slot on both sides was explicitly checked via the isSparse assignment before being consumed or skipped, so the invariant holds. The finder-raised concern about the dense-path bounds check over-constraining when later expected indices are holes was also examined — the check uses expected.length (not remaining own-key count), but any hole in expected triggers the sparse fallback before the dense bound is applied to it, so it can't over-reject.

Comment thread src/js/node/assert.ts
@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

CI build #75248 finished with 281/286 jobs green. The new assert-partial-deep-strict-equal.test.ts passed on every lane. The three tests that stayed red are unrelated to this diff and are failing on main:

This change touches only assert.partialDeepStrictEqual's compareBranch internals and has no path into HTTP, net, GC, or process lifecycle. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant