Skip to content

test: restore 6 hand-weakened vendored tests and fix the compat bugs they hid - #35408

Draft
cirospaciari wants to merge 188 commits into
mainfrom
claude/node-v26-hidden-gaps
Draft

cirospaciari wants to merge 188 commits into
mainfrom
claude/node-v26-hidden-gaps

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

Stacked on #34660 — review that first.

What this does

It adds no new test files. It makes six already-vendored ones honest.
The diff modifies eight vendored test files in all: those six, plus
test-dns-lookup.js and test-eventtarget.js, which are adjusted but not
restored verbatim (see below).

Twenty files under test/js/node/test/parallel/ had been hand-edited before they
were vendored: assertions commented out, // Flags: headers double-commented,
whole blocks deleted, common.mustCall() wrappers stripped. Each edit was hiding
a real Bun bug. Replacing any of the twenty with the byte-verbatim Node v26.3.0
file makes it fail.

This PR restores five of them verbatim and fixes the bug each one was concealing.
A sixth, test-c-ares.js, was weakened the same way and turned up when CI ran the
dns fixes; it is restored verbatim too.

The bugs that were hidden

test-dns.js (59 assertion-ish lines removed) hid five defects in node:dns:

  • c-ares was caching answers. Node passes ARES_OPT_QUERY_CACHE with
    qcache_max_ttl = 0, which turns the cache off. Bun passed neither, so c-ares
    used its own default and served cached answers for up to an hour — a second
    resolve() of the same name and record type never reached the resolver. This
    is the fix in src/cares_sys/c_ares.rs.
  • dns.lookup('') resolved to null instead of failing. That was the DEP0118
    compatibility path, which Node removed in v26: an empty or falsy hostname is
    now ERR_INVALID_ARG_VALUE (thrown by the callback form, rejected by the
    promise form).
  • dns.resolve*() reported their first argument as hostname. Node's shared
    resolver names it name.
  • dns.resolve*() rejected the optional middle options argument that Node
    accepts and ignores, so dns.resolveSoa(name, undefined, cb) threw
    "callback must be a function". resolveCaa/resolveTxt/resolveSoa also
    never validated their hostname at all.
  • dns.lookupService() with a non-IP address produced the native resolver's own
    message instead of Node's ERR_INVALID_ARG_VALUE, and options.family
    produced a hand-written message instead of validateOneOf's.

test-c-ares.js (not in the original list — CI surfaced it) hid the same
DEP0118 removal plus one more: dns.resolve() passed any string rrtype straight
to the resolver, so an unsupported record type surfaced as a callback-validation
error rather than Node's ERR_INVALID_ARG_VALUE. Its two dns.lookup(null)
assertions had been rewritten to expect the removed DEP0118 result and its rrtype
message assertion was commented out.

test-events-customevent.js (8 lines) hid two:

  • new CustomEvent(type, nonObject) threw ERR_INVALID_ARG_TYPE without the
    Received type string ('foo') suffix every other ERR_INVALID_ARG_TYPE
    carries.
  • Event had no util.inspect.custom, so util.inspect(ev, { depth: -1 }) gave
    '[Event]' where Node gives 'Event'. The hook added here handles only
    that negative-depth case and returns this otherwise, so Bun's own (richer
    than Node's) event formatting is untouched — see the CI note below.

test-util-deprecate.js (9 lines) hid one: util.deprecate accepted Node
v26's new { modifyPrototype } option and ignored it. (Bun also compiles builtin
modules such that function declarations get no own prototype property, so the
wrapper needs one defined explicitly to look like a plain function.)

test-timers-refresh.js (4 lines) hid a missing internal:
internal/timers had no setUnrefTimeout. net and http2 each hand-rolled
setTimeout(...).unref() at the exact call sites where Node calls it.

test-tls-translate-peer-certificate.js (13 lines) hid nothing functional —
the test had been rewritten to import _tls_common because Bun had no
internal/tls/common. Upstream keeps the implementation in
internal/tls/common and re-exports it from the deprecated _tls_common; Bun
now mirrors that, so the implementation has one home.

What the first CI run caught, and what changed because of it

The first push went red on eight lanes. Both headline changes had collateral, and
all of it is fixed here:

  • test-c-ares.js was asserting the DEP0118 result the dns fix removes. It is
    a hand-weakened file in the same class, so it is restored verbatim and the
    rrtype gap it also hid is fixed. It now passes.
  • node-dns.test.js's promisify check compared two live lookups of
    google.com and only ever agreed because c-ares was caching the answer — the
    exact defect this PR fixes. It now uses an IP literal, which short-circuits the
    resolver and cannot round-robin. The c-ares fix itself stands: Node disables the
    cache deliberately.
  • The Event inspect hook was too broad. Replacing Bun's formatting outright
    dropped ErrorEvent's message/error and MessageEvent's data from
    Bun.inspect output, breaking inspect.test.js and
    websocket/error-event.test.ts. It now only answers the negative-depth case and
    returns this for everything else.
  • Returning this then exposed a pre-existing inspector bug: the native
    inspector lists non-enumerable symbol properties, so the hook printed itself
    back out. Node never shows it (it is non-enumerable). Both enumeration paths in
    bindings.cpp now skip it when it is non-enumerable — which also fixes any user
    class whose util.inspect.custom returns this.
  • Node's inspector skips the custom hook when the value is its constructor's
    prototype; Bun's does not, so Bun.inspect(Event.prototype) threw
    ERR_INVALID_THIS. The hook now hands the value back instead of throwing.
  • test-eventtarget.js had a Bun branch asserting '[Event]'. It now matches
    upstream and that branch is deleted.
  • bun-run-dir.test.ts was tagged [flaky] by the runner on windows-11-aarch64
    and passed on retry; it does not touch anything here.

Verification

Each restored file passes as one process, the way CI runs it:

BUN_GARBAGE_COLLECTOR_LEVEL=1 BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 NO_COLOR=1 \
  ./build/debug/bun-debug run --config=bunfig.node-test.toml <file>

Each was also canaried (append ;throw new Error(…), confirm the file then
exits non-zero), so none of them is passing by skipping.

Regression sweeps, all green: every test-dns* file (25), every
test-event*/test-eventtarget*/test-util-inspect*/test-util-format*/
test-abort*/test-whatwg-event*/test-messageport*/test-broadcastchannel*
file, every test-net* and timer/timeout file, test/js/bun/util/,
test/js/node/util/ (19 files), test/js/web/websocket/error-event.test.ts,
test/js/node/dns/node-dns.test.js, test/js/node/stubs.test.js,
test-warn-tls-common-deprecation, test-internal-module-require,
test-http2-compat-socket, test-runner-mock-timers,
test-http-client-timeout-on-connect.

Two failures in those sweeps are pre-existing and unrelated: parseArgs stress
and no assertion failures 2 are debug+ASAN timeouts, and
test-worker-message-port-wasm-threads needs WebAssembly shared memory.

Two files that are not byte-verbatim, on purpose

  • test-dns-lookup.js asserted the DEP0118 behaviour this PR removes, so it
    had to be updated. It is re-synced against upstream v26.3.0 and now differs by
    two adaptations instead of the previous 45 lines: Bun has no
    internalBinding('cares_wrap'), so the ENOMEM injection goes through
    Bun.dns.lookup instead, and the expectWarning for the
    internal/test/binding warning that Bun never emits is dropped. preflight.sh
    flags this file as DRIFT for that reason.
  • test-eventtarget.js was already 333 lines adrift; this only deletes the
    five-line Bun branch that expected '[Event]', bringing it closer to upstream.
  • test/js/node/test/common/index.js loses its hand-written
    internal/timers stub, which the real module now supersedes.

test/js/node/dns/node-dns.test.js also moves off the DEP0118 expectations and
onto the v26 errors; both new expectations were checked against real Node
v26.3.0.

test/expectations.txt is untouched.

Still hidden — the other fifteen, with the blocker for each

These were each restored verbatim, confirmed failing, and reverted rather than
left broken in the tree. The failure named is the first one; several have more
behind it.

file first blocker
test-util-types.js internalBinding('js_stream') and friends are not implemented
test-assert.js with Error.stackTraceLimit = 0, assert.ok() cannot read its call site, so the message is false == true instead of The expression evaluated to a falsy value: …
test-crypto-key-objects.js importing an RSA private JWK where n !== p * q is accepted; Node throws ERR_CRYPTO_INVALID_JWK
test-crypto-sign-verify.js crypto.sign(algo, data, { key, context }) — the Ed25519ph/Ed448 context parameter is unsupported
test-crypto-dh-stateless.js the whole file is commented out in the vendored copy; crypto.diffieHellman misses the ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE and key-format validation Node does, and the error messages differ
test-eventtarget.js still not verbatim: dispatchEvent/addEventListener produce WebIDL messages (Argument 1 ('event') to EventTarget.dispatchEvent must be…) rather than Node's ERR_INVALID_ARG_TYPE text; also wants events.listenerCount(eventTarget, …) and internal/event_target's defineEventHandler
test-stream-finished.js internal/streams/end-of-stream and its kEosNodeSynchronousCallback option (settle the callback synchronously when eos() can) do not exist; several destroy() implementations are also never called
test-blocklist.js BlockList has no toJSON/fromJSON and no util.inspect.custom, so inspect(bl, {depth:-1}) prints BlockList {} instead of [BlockList]. It is a Rust-native class, so this is a Rust change
test-util-inspect-proxy.js no getProxyDetails binding and no showProxy support: inspect(proxy) prints the target, never Proxy(…), and a proxy whose traps throw makes util.format('%s', …) throw
test-webcrypto-sign-verify.js WebCrypto has no KMAC128/KMAC256 or ML-DSA-44/65/87
test-webcrypto-wrap-unwrap.js same: unrecognized algorithm during generateKey
test-tls-ocsp-callback.js the OCSP status callbacks never fire
test-tls-session-cache.js drives the OpenSSL s_client binary through the newSession/resumeSession server events
test-child-process-spawnsync-shell.js monkeypatches internal/child_process.spawnSync and expects the public API to route through it; also wants the DEP0190 warning for args passed with shell: true
test-child-process-spawnsync-kill-signal.js same monkeypatching of internal/child_process.spawnSync

The c-ares caching defect is the one worth flagging beyond this PR: it applied to
every dns.resolve* call in Bun, and it was invisible because the only test that
would have caught it had the relevant assertions removed.

cirospaciari and others added 30 commits July 18, 2026 13:06
…ughtException

These callbacks are dispatched from a promise reaction, so a throw inside one
rejected the derived promise and surfaced as an unhandledRejection instead of an
uncaughtException as it does in Node, where the callback runs off the libuv
request completion. process.on('uncaughtException') and node:domain never saw
errors thrown from fs, dns, or crypto.pbkdf2 callbacks.

Add guardCallback in internal/shared and apply it at the existing callback
chokepoints (ensureCallback in fs, validateResolve in dns) plus the few sites
that bypass them. The callback keeps its exact place in the event loop; only the
throw is rerouted, through the same native path timer callbacks already use,
which is why setTimeout was never affected.

opendir and glob already hop through process.nextTick for this and are left
alone, since routing them would change their timing.
Found by sweeping every upstream test absent from both main and all 47 open
pull requests, so none of these overlap work in flight. No source change is
needed for any of them.

  sequential/test-cpu-prof-default
  sequential/test-cpu-prof-dir-absolute
  sequential/test-cpu-prof-dir-and-name
  sequential/test-cpu-prof-dir-relative
  sequential/test-cpu-prof-drained
  parallel/test-disable-sigusr1
  parallel/test-whatwg-webstreams-adapters-to-writablestream

Each was run 6 times, checked for a skip marker, and tamper-checked: mutating a
mustCall count or an assertion makes every one of them exit non-zero, so none is
passing vacuously.

No-Verification-Needed: test-only diff, no runtime surface to drive
  test-pipe-stream
  test-pipe-unref
  test-queue-microtask-uncaught-asynchooks

All three bind unix or UDP sockets, so an earlier sweep that ran them under a
restricted sandbox recorded them as flaky and they were wrongly discarded. With
the sandbox off they are 6/6, verbatim. CI is unaffected by that restriction.

The two pipe tests are tamper-checked: mutating an assertion exits 1. The
queue-microtask one is not, because its only assertion runs inside a
process.on('exit') handler and a failure there does not change the exit code --
but that is equally true on the node v26.3.0 binary, so there is no
bun-versus-node differential. It still exercises async_hooks, queueMicrotask and
uncaughtException together and would catch a crash or a hang.
Upstream tests run with --expose-internals import node's internal modules
directly. Bun already has the mechanism (exposedInternals in
internal-for-testing.ts); this widens it to more of Bun's REAL modules under the
names node's tests use:

  internal/validators        -> src/js/internal/validators.ts
  internal/util/inspect      -> src/js/internal/util/inspect.js
  internal/util              -> { normalizeEncoding } from node_util_binding
  internal/errors            -> bun's internal/errors.ts (aggregateTwoErrors only)
  internal/event_target      -> native Event/CustomEvent/EventTarget + kWeakHandler
  internal/freelist, internal/fixed_queue, internal/assert/myers_diff

internal/net is recovered in the harness by probing net._normalizeArgs([]) for
the module-private normalizedArgsSymbol rather than minting a look-alike.
hasTemporal and hasLocalStorage are feature-detected in common/index.js; both
are false here.

Nothing above is a test-only construct: each name maps to the same code users
hit. Modules with no Bun counterpart were deliberately not shimmed -- the rest
of internal/errors is native C++ factory functions rather than a JS codes table,
so E/SystemError/hideStackFrames would be a second error hierarchy and the tests
would measure the shim rather than bun.

Adds test-global-customevent, test-net-normalize-args and
test-whatwg-encoding-singlebyte (57 subtests) with its fixtures, verbatim.
Found by re-sweeping the 754 upstream tests still absent from main and every
open pull request, against a build carrying this branch's callback-dispatch and
internal-module changes. Passes as-is, no source change.

6/6 with no skip marker, byte-identical to upstream, and tamper-checked: a
mutated assertion exits 1.

No-Verification-Needed: test-only diff, no runtime surface to drive
A `git add -A test/js/node/test` on an earlier commit swept in files that were
never meant to land here: a scratch file, a test previously rejected as vacuous
(test-gc-net-timeout), one that belongs to another branch and fails
`handle instanceof UDP` on Windows (test-dgram-create-socket-handle), one that
times out on every Windows shard (test-pipe-unref), and seven files that
duplicate the free-wins branch (test-cpu-prof-*, test-disable-sigusr1,
test-whatwg-webstreams-adapters-to-writablestream). All removed; each lives on
in the branch that owns it, or nowhere, as appropriate.

Also fixes CI-visible test-order dependence in the callback-throw suite: every
subtest shared one tempdir, so the symlink subtests' l3/l4/lnc entries could
become the first entry the Dir subtest's dir.read() returns. The Dir subtest now
uses its own directory.
Node exposes formatTime from internal/util/debuglog and its test imports it
from there under --expose-internals. Bun's copy was a closure inside the
ConsoleObject builtin, unreachable by name. It now lives in
internal/util/debuglog, required by createConsoleConstructor the same way that
function already requires internal/validators, and is exposed through
exposedInternals.

Output is unchanged: the algorithm is line-for-line the same and the module is
loaded lazily by the console.Console getter, so global console.timeEnd, which
is native, is untouched. Plain prototype methods replace the captured-$call
style because that is the internal-module convention, per neighboring modules.

Adds test-console-formatTime from Node v26.3.0, verbatim.
Deprecations, matching node's exact text and once-per-process semantics:
_tls_common and _tls_wrap emit DEP0192 (the latter previously aliased straight
to node:tls with nowhere to warn, so it is now node's re-export shim, and plain
require("tls")/require("https") stay silent). --pending-deprecation and
NODE_PENDING_DEPRECATION gate DEP0111 on process.binding() and DEP0119 on
binding("uv").errname. Bun's own builtins call process.binding() where node's
internals use internalBinding, so internal callers neither warn nor consume the
once-latch; a later user call still warns exactly once.

cpu-prof: --cpu-prof-name/-dir/-interval without --cpu-prof exit 9 with node's
message instead of warning and continuing, with an interval equal to the
default treated as a noop as node does. ${pid} is substituted in
--cpu-prof-name. The default filename is node's
CPU.<yyyymmdd>.<hhmmss>.<pid>.<tid>.<seq>.cpuprofile in local time; the old name
used epoch microseconds, and the first cut of this exposed that
bun_core::Timespec is not wall-clock (it produced 1970), so the timestamp comes
from SystemTime with localtime_r, or GetLocalTime on Windows. A self-directed
fatal signal via process.kill with no JS listener flushes the profile before
dying, as node's Kill binding does; the process still dies by the real signal.

Not converted, deliberately: DEP0169 on url.parse was removed on purpose in
PR #16641, so re-adding it would reverse a product decision; and SIGPROF is not
reserved by JSC, so node's "reserved while debugging" warning would claim a
behavior bun does not have.

Adds six upstream Node v26.3.0 tests, verbatim.
…G_SNAPSHOT

getHeapStatistics gains total_allocated_bytes so the key set matches node's
exactly. getHeapSpaceStatistics and getHeapCodeStatistics report JSC's real
totals in node's shape instead of throwing: JSC has one undivided heap, so its
used and capacity numbers appear under old_space and the other twelve V8 space
names carry zeros; the names are required by shape-dependent consumers, the
numbers are real. getCppHeapStatistics validates its argument with node's
message and returns the empty statistics node itself reports when cppgc holds
nothing.

setFlagsFromString validates and records the flags instead of throwing, and
cachedDataVersionTag derives a stable uint32 from the runtime version plus the
recorded flags, so the tag changes across a setFlagsFromString call as node's
does. startupSnapshot callbacks throw node's ERR_NOT_BUILDING_SNAPSHOT.

writeHeapSnapshot path handling is deliberately untouched: PR #34084 already
fixes it. queryObjects is not implemented; it needs real heap iteration by
prototype chain, which JSC does not expose.

Adds test-v8-stats, test-v8-version-tag, test-v8-startup-snapshot-api and
test-cppheap-stats from Node v26.3.0, verbatim.
--heap-prof-name/-dir/-interval without --heap-prof now exit 9 with node's
"<argv0>: <flag> must be used with --heap-prof" instead of warning and
continuing; --heap-prof-interval was previously not registered at all, so
`bun --heap-prof-interval 128 x.js` parsed 128 as the script. An interval
equal to node's default (512 KiB) is a noop without --heap-prof, like node.

--heap-prof now writes node's V8 sampling-heap-profile JSON
({"head": {...}, "samples": [...]}) under node's filename format
Heap.<yyyymmdd>.<hhmmss>.<pid>.<tid>.<seq>.heapprofile in local time (the old
name also hit the same non-wall-clock Timespec bug the CPU side fixed).
JavaScriptCore has no allocation-site sampler, so the profile reports the
real live-heap size on the (root) frame with no per-function attribution;
the full .heapsnapshot remains available via v8.writeHeapSnapshot() and
Bun.generateHeapSnapshot("v8"). The markdown format (--heap-prof-md) is
unchanged. Heap profiles also flush on a self-directed fatal signal via
process.kill, sharing the CPU profiler's hook.

Writing to an absolute --heap-prof-dir or --heap-prof-name (and an absolute
--cpu-prof-name) previously hit a debug assertion in AutoAbsPath::append;
both writers now use resolve-style join.

Adds two upstream Node v26.3.0 tests, verbatim. The other nine
test-heap-prof-* files assert a runAllocation call frame inside the profile
tree (or per-worker profiles), which requires allocation-stack sampling JSC
does not provide, so they are not vendored.
The vendored harness now parses node's `// Env: A=1 B=2` comment convention:
parseTestMetadata mirrors upstream, and the test re-spawns itself with the
merged environment when any variable differs, as node's common does. Two gaps
that respawning exposed are fixed with it: the --expose-internals interceptor
now installs when the flag arrives via execArgv in a re-spawned child, and the
internal/options shim resolves --test-isolation from execArgv.

node:url exports URLPattern, which node v26 re-exports from the global.

pathToFileURL is ported from node's own JS implementation: the windows option,
UNC and extended-UNC edges, node's pre-encode character set, and posix
backslashes kept as %5C, which bun dropped. fileURLToPath attaches err.input,
the parsed URL object, on ERR_INVALID_FILE_URL_PATH. The Bun.pathToFileURL and
Bun.fileURLToPath globals are untouched.

The exposed internal/util/inspect.getStringWidth now implements node's
per-code-point east-asian-width-first algorithm, so a family emoji measures 8
and a skin-tone emoji 4 as in node. Bun.stringWidth and bun's own console
layout are deliberately unchanged; bun measures grapheme clusters and that is a
product decision, not a test's to overturn.

Adds seven upstream Node v26.3.0 tests, verbatim: the three Env-comment tests,
test-urlpattern, test-icu-stringwidth, and the two file-URL conversion tests.
A getcwd failure at startup aborted every entry point with a generic
ENOENT. Node falls back to the executable's directory
(Environment::GetCwd in env.cc) and lets process.cwd() surface the
error later.

- add bun_core::getcwd_or_exe_dir and use it for the synthetic entry
  paths (-e/-p, stdin, cron, node-emulation eval and relative scripts,
  feedback) and for the parsed startup cwd of runtime commands only;
  install/test/build keep the hard error so they never act on a tree
  found above the executable
- an absolute --cwd no longer needs a live cwd; a relative one still
  errors; the stored cwd is the post-chdir physical path so
  process.cwd(), path.resolve, and the resolver agree
- route a bare --interactive to the REPL like node's --interactive

Enables test-cwd-enoent{,-preload,-repl}.js (verbatim from Node
v26.3.0).
process.cwd() returned the resolver's cached top_level_dir, so it kept
returning a stale path after the directory was rmdir'ed. Node re-runs
uv_cwd on every cache miss and clears its cache on chdir
(lib/internal/bootstrap/switches/does_own_process_state.js).

- Bun__Process__getCwd now does a real getcwd and on failure throws
  Node's uv_cwd UVException (message, code, errno, syscall verified
  byte-identical against node v26.3.0)
- Process_functionChdir clears the cached cwd instead of repopulating
  it, so the next process.cwd() re-queries the OS
- WriteStream's internal $fastPath no longer path.resolve()s its
  discarded path, so child_process spawn keeps working from a deleted
  cwd (its stdin wiring constructs WriteStream(""))
- drop the now-unused node::path get_cwd alias

The hot path is unchanged: the cached JSString still serves repeated
process.cwd() calls; only the first call after startup or chdir pays
the syscall.

Enables test-cwd-enoent-improved-message.js (verbatim from Node
v26.3.0).
A failed require or import threw ResolveMessage with bun's own message and no
code, so the ubiquitous userland pattern `err.code === 'MODULE_NOT_FOUND'`
never matched. ResolveMessage now extends Error via a new prototypeBase class
option in codegen -- the direction NodeUtilTypesModule.cpp's FIXME already
sanctions -- and its message getter returns node's exact text for runtime
import kinds: MODULE_NOT_FOUND with a Require stack and a requireStack array
for CJS, ERR_MODULE_NOT_FOUND with the specifier truncated to the package name
for bare ESM imports. The class itself is kept: Bun.build's log API documents
name and constructor as ResolveMessage, and the getter is prefix-gated so
invalid-URL, data-URL and ENAMETOOLONG texts are untouched, as is the CLI
stderr display text.

require.resolve now validates options.paths with node's exact
ERR_INVALID_ARG_TYPE and ERR_INVALID_ARG_VALUE messages.

Nine assertions across three bun-owned test files are updated to the node
shapes; each is listed in the pull request for sign-off.

Adds test-require-resolve-invalid-paths from Node v26.3.0, verbatim.
…stack/callback-throw-dispatch

# Conflicts:
#	src/js/node/dns.ts
- CommonJS.ts: read options.paths once into a local instead of three
  property reads (oxlint single-read rule; also tamper-resistant against
  getters)
- BunCPUProfiler.rs: replace open-coded mem::zeroed with the audited
  bun_core::ffi::zeroed wrapper and use raw-pointer borrows for the
  localtime_r out-params
- bun_core: add Zeroable impl for libc::tm
Implements node v26's Assert class: the namespace is built from
Assert.prototype, options live behind a private symbol so destructured methods
fall back to defaults, calling without new throws ERR_CONSTRUCT_CALL_REQUIRED,
and the diff option is threaded through assertion_error.ts, where full disables
truncation and partialDeepStrictEqual appends diffs to custom messages.

Deep equality gains a third checkPrototypes template flag enabled ONLY by the
new node entry point: node:assert's strict variants and util.isDeepStrictEqual
now compare prototypes (Buffer vs Uint8Array, subclass vs base are unequal, as
in node) while Bun.deepEquals and every expect() instantiation are verified
byte-identical in behavior before and after. Also fixed on the node-gated path:
own-enumerable-only property walks, boxed Symbol/BigInt values, boxed-string own
properties, RegExp lastIndex, invalid dates comparing equal, own properties of
Dates/Maps/Sets/arrays/typed arrays, and WeakMap/WeakSet/Promise never equal.
All twelve documented strictBug entries in bun's own deep-equal tests now pass
and their markers are removed.

Standalone fix worth its own line: the Rust inspect newline splitter emitted a
trailing empty chunk for strings ending in a newline, breaking assertion diff
line counts everywhere.

One bun-owned behavior change, flagged for sign-off: assert !== assert.ok now,
matching node 25+, as a consequence of the prototype layout; assert.spec.ts is
updated accordingly.

Adds test-assert-class, test-assert-class-destructuring and
test-util-isDeepStrictEqual from Node v26.3.0, verbatim.
…mordial-safe

Node's deprecate() closures live in each Environment's own JS, so every
worker warns once itself. The function-local statics latched process-wide
and raced across JS threads; move them onto the Process object.

Also restore the captured prototype methods formatTime used before it
moved out of ConsoleObject.
Node's ESM loader classifies specifiers platform-independently, but the
module-not-found message shaping used the host-native package-path check,
whose Windows arm accepts any byte as a drive letter. A specifier like
':://x' was therefore reported as "Cannot find module" on Windows and
"Cannot find package '::'" on POSIX (Node prints the latter everywhere).
Classify with a host-agnostic predicate that only treats relative forms,
separator-led paths, and ASCII-letter drive forms as path-like.
robobun added 25 commits July 26, 2026 07:34
…oint in the port stays invalid

applyIDNADeltaToURLAuthority previously passed host[:port] to the delta. The
mapping-class entries identity-preserve a port (non-digit in, non-digit out),
but the ignored-class entries (U+180E, U+206A..U+206F) are stripped, turning
`http://foo:8\u180E0/` into `http://foo:80/` where node and pre-PR bun
reject it. Find the last ':' in the non-bracketed host span and leave the
port verbatim so the WHATWG port state still sees the invalid code point.
url.test.ts covers both a rejected port and an accepted host with :80.
…set atomically

The pre-execve disposition reset is process-wide but other threads are not
quiesced; JSC's SamplingProfiler and concurrent GC pthread_kill
sigThreadSuspendResume (an RT signal) and the wasm trap handler catches
SIGSEGV/SIGBUS. Resetting those to SIG_DFL while heap-allocating argv/envp
turns a sampler tick or stop-the-world into a fatal signal under
--watch --cpu-prof. Skip SIGSEGV/SIGBUS and s >= SIGRTMIN so execve resets
them atomically (BunProcess.cpp already guards sigThreadSuspendResume for
the same reason).

DOMURL: note at the is8Bit early return that a percent-encoded delta source
is intentionally excluded from this stopgap.
…e header matches node

kMethodsWithCustomMessageDiff routed partialDeepStrictEqual failures through
createErrDiff, but kReadableOperator had no matching entry, so the header
rendered as the literal string 'undefined'. Added the entry with node
v26.3.0's exact wording; assert.test.cjs asserts the header.
…d url.host setter delta to the host span

bindings.cpp: the array path's pre-existing symbol-only loop (prototype-
walking get/getIfPropertyExists) ran before nonIndexOwnPropertiesEqual,
which already enumerates own enumerable strings and symbols with own-slot
lookups. Under checkPrototypes that meant each symbol getter fired twice per
side where node fires once. Moved the nonIndexOwnPropertiesEqual tail-call
above the symbol block so the node path bypasses the redundant pass;
Bun.deepEquals (non-checkPrototypes) keeps the existing behavior.
deep-equal.test.ts asserts the getter-call count.

URLDecomposition.cpp setHost: same port-span bounding as c0e2643 applied to
applyIDNADeltaToURLAuthority. The delta now runs on value.left(reverseFind
(':')) only, so an ignored-class code point in the port is not stripped into
a valid digit run. url.test.ts covers both a delta-in-port (port stops at
the non-digit) and a delta-in-host (host mapped, port kept).
…LOG_ENABLED

collect_persist_jobs and write_persist_job_locked built display_name() and
format!() Strings per entry even with logging off; those are only read by
cclog!() which already gates on LOG_ENABLED. Gated the bindings like
read_cache_file already does and inlined the Phase-2 failure log args.
The NODE_CHANNEL_FD unset_cloexec and caught-signal SIG_DFL reset were inside
on_before_reload_process_linux under an OS(LINUX) || OS(FREEBSD) gate, so
macOS skipped the hook entirely: usockets CLOEXEC'd the IPC fd and execve
closed it, and the reloaded image attached to a dead fd (the "IPC to the
parent survives a --watch reload" test failed on darwin). Renamed to
on_before_reload_process_posix under !OS(WINDOWS); the close_range sweep
stays Linux/FreeBSD-only, and reload_process calls the hook on all unix.
formatWhatwgURL rebuilt the href from .search/.hash, which both return ""
for a null and an empty-string component, so url.format(new URL("http://x/?"),
{unicode: true}) dropped the marker where node keeps it. Derive presence
from the href (the only bare # is the fragment delimiter; the char before
it or end-of-href is ? iff the query is present-and-empty).
CachedBytecode::__bun_jsc_generate_cached_bytecode and
RuntimeTranspilerCache::OutputCode::byte_slice are called from
NodeCompileCache.rs / jsc_hooks.rs (different crate than the pub(crate)
sweep narrowed them to); node_process get_cwd uses bun_sys::getcwd directly
(Syscall alias removed); repl editor_mode bool renamed to input_mode enum.
…g does not trip hasInlineStorage

{...process.binding("uv")} (internal/test/binding.ts) asserts
hasInlineStorage() in JSObject::inlineStorage on a constructEmptyObject(...,
0) object under the merged WebKit. Use the default-capacity overload; the
binding carries ~85 properties anyway so zero inline was a non-optimization.
…ered Error.* in isInsideNodeModules

isInsideNodeModules read Error.captureStackTrace/stackTraceLimit/
prepareStackTrace at call time inside a try/finally with no catch, so a
deleted captureStackTrace or a non-writable stackTraceLimit made url.parse
throw. Capture captureStackTrace at module scope (matching assertion_error.ts
/inspect.js/quic.ts) and swallow any throw from the body and the restore so
callers never observe it.
The hoisted deliverCallbackResult/Error + .bind(cb) pattern allocated one
bound-function object per .bind call (same count as the arrows it replaced),
so the 'no closure allocates per invocation' comment was wrong; and .bind
resolves through user-mutable Function.prototype.bind where arrows do not.
…-uncaught

# Conflicts:
#	src/runtime/dispatch.rs
#	src/runtime/node/node_fs.rs
…SIGPWR on Linux) in the pre-execve disposition reset

ipc.rs: the 276e176 merge moved the cpp bridge out of crate:: scope; the
two advanced-buffer wrappers now resolve through bun_jsc::cpp like their
ipc_serialize/ipc_parse siblings.

c-bindings.cpp: on Linux g_wtfConfig.sigThreadSuspendResume is SIGPWR (30),
not an RT signal, so the s >= SIGRTMIN break did not skip it; resetting
SIGPWR to SIG_DFL while the SamplingProfiler/concurrent GC are still
pthread_kill'ing the JS thread with it terminates the process instead of
reloading. Skip it explicitly under OS(LINUX), mirroring BunProcess.cpp's
process.on guard.
…into claude/node-v26-hidden-gaps

# Conflicts:
#	src/cares_sys/c_ares.rs
… the try so a throwing getter is swallowed too
…soned CallSite.prototype.getFileName degrades to false instead of escaping to url.parse
…into claude/node-v26-hidden-gaps

# Conflicts:
#	src/js/node/util.ts
Base automatically changed from claude/callback-throw-uncaught to main August 7, 2026 20:35

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants