Skip to content

fs: port Node.js v26.3.0 fs tests and fix the gaps they surface — cp error semantics, watcher event delivery, watch ignore+AbortSignal, FileHandle pull/writer, glob port, opendir/Dir, mkdtempDisposable, rmdir-recursive end-of-life, mock.fn (+119 tests) - #31830

Merged
cirospaciari merged 1 commit into
mainfrom
ciro/port-node-fs-tests
Jun 19, 2026

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jun 4, 2026

Copy link
Copy Markdown
Member

Brings node:fs compatibility in line with Node v26.3.0 by porting upstream tests verbatim and fixing the gaps they expose.

parallel sequential total
fs 327/342 (95.6%) 4/4 331/346 (95.7%)

119 verbatim upstream tests added, 37 refreshed to their v26.3.0 content, 5 retired (the rmdir-recursive* tests Node deleted at DEP0147 end-of-life). Excluding the --expose-internals tests that cannot run by design, coverage is 331/333 (99.4%).

Behavior changes

  • fs.cp/cpSync/promises.cp error semantics: node's full validation layer (validateCpOptions, getValidMode) and SystemError-shaped ERR_FS_CP_* codes (EINVAL, EEXIST, DIR_TO_NON_DIR, NON_DIR_TO_DIR, SOCKET, FIFO_PIPE, SYMLINK_TO_SUBDIRECTORY, UNKNOWN, ERR_FS_EISDIR), v26 message wording, errorOnExist on existing directories, and the directory-gated symlink-subdir checks. The native clonefile fast path is now used only for plain regular-file→file copies; symlinks, directories, and special files route through the node-ported walker so relative symlink targets are resolved the way node resolves them. The callback form validates synchronously and calls callback(null) on success.
  • fs.watch event delivery: the per-handler duplicate suppression in path_watcher.rs/win_watcher.rs suppressed different files' same-type events within the same millisecond (the hash was only consulted when the event type differed), so two files written back-to-back delivered only one event. It now suppresses exact duplicates only — node delivers both.
  • fs.watch/fs.promises.watch ignore option (string glob with matchBase, RegExp, function, or array) with node's ERR_INVALID_ARG_TYPE/ERR_INVALID_ARG_VALUE validation, and AbortSignal support on the promises async iterator (ABORT_ERR with cause).
  • FileHandle.prototype.pull/pullSync/writer on top of main's node:stream/iter (--experimental-stream-iter-gated; the vendored tests carry the flag in their // Flags: header and re-spawn through it).
  • fs.glob/globSync/promises.glob replaced with a faithful port of node's lib/internal/fs/glob.js over node:fs readdir/lstat, with node's deps/minimatch vendored verbatim — fixes extglobs, .//.. pattern segments, trailing-slash directory semantics, brace+** interaction, withFileTypes, exclude-function semantics, and error propagation from a throwing callback (dispatched once via process.nextTick). The header documents why this isn't backed by Bun.Glob yet (328/448 of test-fs-glob.mjs fail with the Bun.Glob.scan-backed version on main — withFileTypes Dirents, exclude-array rules, symlink walking, dotfile rules); to be swapped once those gaps close natively.
  • fs.opendir/Dir: eager ENOTDIR/ENOENT at open, bufferSize and encoding validation, ERR_INVALID_THIS brand check on the path getter, promise-form close(), node's operation queue (ERR_DIR_CONCURRENT_OPERATION for sync ops with async reads in flight), and the async iterator auto-closes on early exit. Entry iteration is index-based (no Array#shift()).
  • fs.mkdtempDisposableSync / fs.promises.mkdtempDisposable (new node API): returns { path, remove, [Symbol.dispose/asyncDispose] }; remove() is idempotent via force and resolves the path eagerly so process.chdir() cannot redirect removal.
  • fs.rmdir/rmdirSync/promises.rmdir with recursive defined now throw ERR_INVALID_ARG_VALUE with node's verbatim "is no longer supported" message (DEP0147 end-of-life in v26; use fs.rm). Bun's own tests are migrated to rm/rmSync, and the five legacy test-fs-rmdir-recursive* vendored tests (removed upstream in nodejs/node@eec03020880) are replaced by the upstream test-fs-rmdir-recursive-error.js.
  • fs.rmSync reports node's ERR_FS_EISDIR (with info/path/syscall) for non-recursive directory removal, including non-ASCII paths the native path mishandled.
  • writeSync accepts the options-object form ({offset, length, position}), validates buffer types with node's ERR_INVALID_ARG_TYPE, and replicates node's error-context assignment contract so accessors installed on Object.prototype observe the error instead of crashing the process.
  • fs.stat rejects with AbortError when called with an already-aborted signal.
  • FileHandle transfer to worker_threads: kTransfer/kTransferList/kDeserialize per node's protocol, with DataCloneError when the handle is in use; the JS Worker wrapper packs/unpacks JSTransferables since bun's structured clone has no native hook.
  • FSWatcher._handle whitebox surface (an FSEvent handle delegating to the native watcher; replacing it trips node's exact ERR_INTERNAL_ASSERTION).
  • node:test's mock.fn/mock.method/mock.getter/mock.setter backed by a node-shaped MockFunctionContext (port of lib/internal/test_runner/mock/mock.js): methodName validated as string|symbol, mockDescriptor.configurable mirrors the original descriptor, and call records are pushed after invocation (node-matching reentrancy semantics).
  • common.isInsideDirWithUnusualChars and common/fs.js added to the vendored test harness; ERR_OPERATION_FAILED added to the error-code registry (with the checked-in ErrorCode.rs discriminants regenerated).

Code style

Every src/js/ file ported from nodejs/node carries a github.com/nodejs/node/blob/<v26.3.0-sha>/... permalink to its source, and the vendored minimatch block is marked third-party (ISC license). All src/js/ additions in this PR use hoisted named functions (or class private methods + .bind), no inline arrow closures.

Known limitations / follow-ups

  • FileHandle transfer is wired only into the Worker constructor (workerData + transferList) and the worker-side workerData reception; worker.postMessage({fh}, [fh]) / parentPort.postMessage / arbitrary MessagePort.postMessage still hit native structured clone with no FileHandle hook. Better solved natively in a follow-up.
  • mock.fn(MyClass) does not preserve the wrapped class's prototype/statics (node's #setupMock returns a Proxy with construct/apply/get traps; this PR's plain-function wrapper covers function/method mocking but not class-constructor mocking). Tracked for a Proxy-based follow-up.
  • The remaining unvendored parallel tests need --expose-internals/internal/test/binding. test-fs-write.js needs V8 externalizable strings and test-fs-promises.js asserts V8-style at async stack frames — both effectively out of reach on JSC.
  • Recursive fs.cp no longer uses clonefile on macOS (node-correct relative-symlink rewriting requires the walker); plain file→file copies keep the native path. Worth revisiting with a symlink-free fast-path detection if the perf matters.
  • Pre-existing and unrelated: test/js/node/fs/fs.test.ts intermittently panics Deadlock detected in native AsyncReaddirRecursiveTask::perform_work (reproduces byte-for-byte on an unmodified baseline binary), and the readdirSync recursive x 100 tests time out under the debug build.

Testing

Validated locally with the release build before pushing:

  • test-fs-glob.mjs 448/448; full vendored fs sweep (test-fs-* parallel + sequential, runner-equivalent semantics) passes.
  • Bun regression gates: fs/fs.test.ts 265/0, fs/cp.test.ts 43/0, fs/dir.test.ts 20/0, fs/glob.test.ts 27/0, node/watch/, worker.test.ts 24/0, module/node-module-module.test.js 30/0 — 0 failures.
  • Watcher fix verified with a two-file write repro (previously only the first event arrived) and the full 40-test watch subset.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Refactors fs.cp/cpSync with Node-like errors and fast paths, adds fs.watch ignore and AbortSignal handling, implements iterable streams (from/pull/consumers/transform), updates node:fs APIs and runtime/native wiring, introduces experimental node:stream/iter and node:zlib/iter, and adds extensive tests and fixtures.

Changes

Core implementation and test coverage

Layer / File(s) Summary
All implementation, wiring, and tests
src/js/internal/fs/*, src/js/internal/streams/iter/*, src/js/node/*, src/jsc/*, src/runtime/node/*, src/resolve_builtins/*, src/sys/*, tests/**/*, fixtures
Node-compatible cp/watch, iterable streams, Node shims/runtime updates, builtin/module additions, and broad test suite updates/additions.

Suggested reviewers

  • Jarred-Sumner
  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title comprehensively summarizes the main change: porting Node.js v26.3.0 fs tests, fixing gaps with proper cp error semantics, watcher behavior, watch ignore/AbortSignal, FileHandle APIs, glob port, opendir/Dir, mkdtempDisposable, rmdir-recursive removal, and mock.fn addition.
Description check ✅ Passed The PR description comprehensively covers what is being done, how it was verified, and includes detailed behavioral changes, testing validation, and known limitations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator
Updated 6:44 PM PT - Jun 18th, 2026

@cirospaciari, your commit ce686d2 has 1 failures in Build #63416 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31830

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

bun-31830 --bun

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Found 13 issues this PR may fix:

  1. fs.rmSync returns EFAULT instead of EISDIR when removing a directory without recursive #28958 - PR fixes fs.rmSync to report ERR_FS_EISDIR instead of EFAULT for non-recursive directory removal
  2. fs.glob differs from node and incorrectly follows symlinks #29699 - PR replaces fs.glob with a faithful port of Node's glob.js, which does not follow symlinks
  3. fs.glob never matches dot files, even with explicit dot patterns #28021 - PR replaces the Bun.Glob.scan()-backed fs.glob with Node's implementation, which handles dot patterns correctly
  4. fs.glob does not support options.withFileTypes #22018 - PR explicitly adds withFileTypes support in the new glob implementation
  5. Bun's Glob.scan and fs.glob do not detect unix domain sockets #17506 - PR replaces fs.glob with Node's readdir-based implementation, which finds all entry types including sockets (note: Bun.Glob itself is not fixed)
  6. globSync(‘{*/*}’) always returns empty array #24000 - PR fixes brace expansion and ** interaction in the new glob implementation
  7. Bun's globbing with nonexistent cwd throws an error in contrast to Node.js #22628 - PR's Node-faithful glob.js silently yields no results for nonexistent cwd, matching Node behavior
  8. node:fs.watch: Watch sends change event twice #21646 - PR fixes duplicate event suppression to hash event type AND filename, preventing spurious duplicate change events
  9. fs.Dir behaves differently in bun compared to node when using async iteration #28894 - PR adds fs.Dir async iterator auto-close on early exit, matching Node behavior
  10. opendirSync successfully returns for a non-existent directory #17581 - PR adds eager ENOTDIR/ENOENT validation at opendirSync open time
  11. node:fs/promises missing mkdtempDisposable export (Bun 1.3.13) — blocks production CLI startup #31400 - PR implements mkdtempDisposable / mkdtempDisposableSync in node:fs and node:fs/promises
  12. fs.mkdtempDisposable() not found in module node:fs/promises #24499 - PR implements fs.mkdtempDisposable() (same fix as node:fs/promises missing mkdtempDisposable export (Bun 1.3.13) — blocks production CLI startup #31400)
  13. Support node:test mock #24255 - PR implements node:test mock.fn / mock.method with MockFunctionContext

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #28958
Fixes #29699
Fixes #28021
Fixes #22018
Fixes #17506
Fixes #24000
Fixes #22628
Fixes #21646
Fixes #28894
Fixes #17581
Fixes #31400
Fixes #24499
Fixes #24255

🤖 Generated with Claude Code

Comment thread src/js/node/fs.promises.ts Outdated
Comment thread src/js/internal/streams/iter/utils.ts Outdated
Comment thread src/js/node/fs.ts
Comment thread src/js/node/fs.promises.ts
Comment thread src/js/node/fs.ts Outdated
@cirospaciari
cirospaciari force-pushed the ciro/port-node-fs-tests branch from 36e1de4 to d333262 Compare June 5, 2026 01:04
Comment thread src/js/node/test.ts Outdated
Comment thread src/js/internal/streams/iter/consumers.ts

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

Actionable comments posted: 15

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/internal/fs/cp-sync.ts`:
- Around line 246-252: The error objects created by the fsEisdirError(...) calls
incorrectly set errno to EINVAL while code is "EISDIR"; update those
fsEisdirError(...) invocations in cp-sync (the block throwing the directory
error and the other matching block) to use errno: EISDIR so errno and code are
consistent, and make the same change in the async sibling cp where
fsEisdirError(...) is called so both sync and async branches export errno ===
"EISDIR".

In `@src/js/internal/fs/watch.ts`:
- Around line 14-35: The ignore-matcher uses mutable global/prototype methods;
update createIgnoreMatcher to use the module's tamper-resistant intrinsics:
replace Array.isArray with the $-prefixed primordial (e.g. $isArray), use
$StringPrototypeIncludes.call(matcher, "/") instead of matcher.includes, use
$RegExpPrototypeExec.call(matcher, filename) instead of matcher.exec, and use
the primordial-safe function check (e.g. $isFunction) for the function branch;
also ensure basename is called via its primordial-safe reference if one exists
and keep existing error throws ($ERR_INVALID_ARG_VALUE / $ERR_INVALID_ARG_TYPE)
unchanged.

In `@src/js/internal/streams/iter/transform.ts`:
- Around line 18-25: The current makeBufferedTransformAsync (and the sibling
buffered helpers referenced in 25-60, 64-98, 111-143) buffer the entire input
and call a synchronous processFn at EOF, causing O(total input) memory use and a
blocking final step; replace this design with a true streaming transform: create
a Transform that feeds incoming chunks into a streaming codec (e.g. Node zlib
streaming API like zlib.createGzip/createGunzip or any async streaming codec),
pipe chunks through that codec and push emitted chunks immediately to respect
backpressure, and only on stream end ensure the codec is flushed (honoring
emitOnEmpty by deciding whether to emit flush/headers when no input seen);
propagate codec errors and preserve the same function signature
(makeBufferedTransformAsync(processFn, emitOnEmpty)) so callers remain
unchanged, and apply the same streaming rewrite to the other buffered helper
variants mentioned.

In `@src/js/node/fs.promises.ts`:
- Around line 997-1048: The code currently mutates the shared pos and
bytesRemaining before the async write completes in write() and writev(), causing
incorrect state if writeAll/writevAll reject or abort; change the logic so you
compute the intended position and decrement amount locally but do not assign to
the shared pos or bytesRemaining until the write promise resolves successfully:
call writeAll(chunk, ..., position, signal) / writevAll(chunks, position,
signal) first, then in a .then() (or await) update pos and bytesRemaining (using
the local totalSize for writev) and return the resolved value, leaving
pos/bytesRemaining untouched on rejection so state stays consistent. Ensure you
still validate signal (signal.aborted) before calling the async write and
reference the functions/variables write, writev, writeAll, writevAll, pos,
bytesRemaining in your changes.
- Around line 64-65: Validate that options.signal is a real AbortSignal before
using it or creating the native watcher: check the signal (e.g., via instanceof
AbortSignal or the project’s isAbortSignal helper) immediately after const
signal = options?.signal and again before creating the watcher/fs.watch() to
avoid creating a native watcher with an invalid signal; if the signal is not a
valid AbortSignal, throw/ignore appropriately so the pre-abort fast path and the
watcher creation cannot proceed with an invalid value (refer to the local
variable signal and the watcher/fs.watch() creation points).
- Around line 288-295: In rmdir (async function rmdir) ensure you validate that
options.recursive, when present, is a boolean before using it to decide the
error path: if options?.recursive exists but is not a boolean, throw a type
error (using the same error helper pattern) instead of treating truthy
non-boolean values as the "use fs.promises.rm instead" case; only when
options.recursive === true should you throw the $ERR_INVALID_ARG_VALUE message
directing callers to fs.promises.rm, otherwise perform normal type validation
and proceed.

In `@src/js/node/fs.ts`:
- Around line 1137-1140: The iterator finally block should call the async
close() instead of closeSync() to avoid ERR_DIR_CONCURRENT_OPERATION when other
queued async read()/close() operations exist; update the finally in the Dir
async iterator to replace "if (this.#handle >= 0) this.closeSync();" with an
awaited async close (e.g., "if (this.#handle >= 0) await this.close();") or
otherwise schedule this.close() (e.g., return this.close().catch(() => {})) so
teardown uses Dir.prototype.close() rather than the synchronous closeSync()
path.
- Around line 91-94: Restore the callback validation in rmdir(): ensure the user
callback is validated via ensureCallback before it is passed into
nullcallback/fs.rmdir so that calling fs.rmdir(path) without a function throws
the proper fs callback-argument error; specifically, in the rmdir implementation
(look for function rmdir and the nullcallback(callback) usage) call
ensureCallback(callback) (or assign callback = ensureCallback(callback)) prior
to invoking nullcallback(callback) and fs.rmdir so missing/invalid callbacks
produce the intended TypeError.

In `@src/js/node/test.ts`:
- Line 124: The function mockFn currently declares an unused parameter options
which triggers eslint(no-unused-vars); remove the options parameter from the
signature or rename it to a deliberately unused identifier (e.g., _options) so
the linter recognizes it as intentionally unused; update any callers if you
remove the parameter and keep the function name mockFn unchanged.
- Around line 164-198: The restore function currently always redefines the
descriptor on objectOrFunction, which leaves an own property when the original
descriptor was inherited; modify the code that computes target/descriptor to
record whether the descriptor came from the object itself (e.g., capture a
boolean like isOwn = (target === objectOrFunction)), then change restore() so
that if isOwn is true it restores via Object.defineProperty(objectOrFunction,
methodName, descriptor!), otherwise it deletes any temporary own property
created by the mock (e.g., delete objectOrFunction[methodName]); update
createMockFunction invocation and kMockRestorers usage to use this corrected
restore logic so inherited descriptors are not shadowed after restore.

In `@test/js/node/fs/fs.test.ts`:
- Around line 2041-2042: The rejection expectation for promises.rmdir is
currently fire-and-forget; change it to await the assertion (e.g., add await
before expect(promises.rmdir(path, { recursive: true
})).rejects.toMatchObject(...)) and likewise ensure any other .rejects
assertions (such as those involving promises.rm or other async rejection checks)
are awaited or returned so the test actually verifies the rejection rather than
racing to completion.

In
`@test/js/node/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs`:
- Around line 17-21: The test claims to exercise the "force is false" path but
neither cpSync nor cp call sets force:false; update the cpSync call using
mustNotMutateObjectDeep({ dereference: true, recursive: true }) and the cp call
that passes { dereference: true, recursive: true } so both option objects
explicitly include force: false (i.e., { dereference: true, recursive: true,
force: false }) so the cpSync and cp code paths for force=false are actually
exercised; ensure you keep the same wrappers (mustNotMutateObjectDeep and
mustCall) around the modified option objects.

In `@test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs`:
- Around line 28-33: The test races because cpSync(sock, dest) may run before
the server is actually listening; modify the test around server.listen, waiting
for the server to be ready (use the listen callback or server.once('listening'))
and only then call assert.throws(() => cpSync(sock, dest), { code:
'ERR_FS_CP_SOCKET' }) and finally call server.close() inside that readiness
handler; target the server.listen(...) call and the
cpSync/assert.throws/server.close sequence when making this change.

In `@test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs`:
- Around line 1-2: The test claims to exercise cpSync with dereference: true and
force: false but the two cpSync calls (cpSync(..., { dereference: true,
recursive: true })) omit force; update both cpSync calls in
test-fs-cp-sync-dereference-twice.mjs (the two cpSync invocations that currently
pass { dereference: true, recursive: true }) to include force: false so the
option matrix actually covers the silent-fail behavior when force is false while
keeping dereference: true and recursive: true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b72bf81a-0968-495e-a86e-2c72f3d2a2c8

📥 Commits

Reviewing files that changed from the base of the PR and between ef89527 and d333262.

📒 Files selected for processing (205)
  • src/js/internal/fs/cp-sync.ts
  • src/js/internal/fs/cp.ts
  • src/js/internal/fs/glob.ts
  • src/js/internal/fs/watch.ts
  • src/js/internal/streams/iter/consumers.ts
  • src/js/internal/streams/iter/from.ts
  • src/js/internal/streams/iter/pull.ts
  • src/js/internal/streams/iter/transform.ts
  • src/js/internal/streams/iter/types.ts
  • src/js/internal/streams/iter/utils.ts
  • src/js/node/fs.promises.ts
  • src/js/node/fs.ts
  • src/js/node/stream.iter.ts
  • src/js/node/test.ts
  • src/js/node/worker_threads.ts
  • src/js/node/zlib.iter.ts
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/NodeDirent.cpp
  • src/jsc/bindings/NodeValidator.cpp
  • src/jsc/bindings/isBuiltinModule.cpp
  • src/jsc/modules/NodeModuleModule.cpp
  • src/jsc/modules/NodeProcessModule.h
  • src/resolve_builtins/HardcodedModule.rs
  • src/resolve_builtins/HardcodedModule.zig
  • src/runtime/node/path_watcher.rs
  • src/runtime/node/win_watcher.rs
  • test/fixtures/copy/kitchen-sink
  • test/js/bun/bun-object/write.spec.ts
  • test/js/node/fs/cp.test.ts
  • test/js/node/fs/dir.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/fs/glob.test.ts
  • test/js/node/test/common/fs.js
  • test/js/node/test/common/index.js
  • test/js/node/test/common/index.mjs
  • test/js/node/test/common/watch.js
  • test/js/node/test/parallel/test-fs-append-file.js
  • test/js/node/test/parallel/test-fs-chown-negative-one.js
  • test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js
  • test/js/node/test/parallel/test-fs-cp-async-async-filter-function.mjs
  • test/js/node/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs
  • test/js/node/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs
  • test/js/node/test/parallel/test-fs-cp-async-dereference-symlink.mjs
  • test/js/node/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs
  • test/js/node/test/parallel/test-fs-cp-async-dir-exists-error-on-exist.mjs
  • test/js/node/test/parallel/test-fs-cp-async-dir-to-file.mjs
  • test/js/node/test/parallel/test-fs-cp-async-error-on-exist.mjs
  • test/js/node/test/parallel/test-fs-cp-async-file-to-dir.mjs
  • test/js/node/test/parallel/test-fs-cp-async-file-to-file.mjs
  • test/js/node/test/parallel/test-fs-cp-async-file-url.mjs
  • test/js/node/test/parallel/test-fs-cp-async-filter-child-folder.mjs
  • test/js/node/test/parallel/test-fs-cp-async-filter-function.mjs
  • test/js/node/test/parallel/test-fs-cp-async-identical-src-dest.mjs
  • test/js/node/test/parallel/test-fs-cp-async-invalid-mode-range.mjs
  • test/js/node/test/parallel/test-fs-cp-async-invalid-options-type.mjs
  • test/js/node/test/parallel/test-fs-cp-async-nested-files-folders.mjs
  • test/js/node/test/parallel/test-fs-cp-async-no-errors-force-false.mjs
  • test/js/node/test/parallel/test-fs-cp-async-no-recursive.mjs
  • test/js/node/test/parallel/test-fs-cp-async-overwrites-force-true.mjs
  • test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs
  • test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps.mjs
  • test/js/node/test/parallel/test-fs-cp-async-same-dir-twice.mjs
  • test/js/node/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs
  • test/js/node/test/parallel/test-fs-cp-async-socket.mjs
  • test/js/node/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs
  • test/js/node/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs
  • test/js/node/test/parallel/test-fs-cp-async-symlink-over-file.mjs
  • test/js/node/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs
  • test/js/node/test/parallel/test-fs-cp-async-with-mode-flags.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-async-error.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-file-url.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-invalid-mode.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-mode-flags.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs
  • test/js/node/test/parallel/test-fs-cp-promises-options-validation.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-apply-filter-function.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-async-filter-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-dereference-directory.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-dereference-file.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-dereference.js
  • test/js/node/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-error-on-exist.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-file-url.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-mode-flags.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-mode-invalid.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-nested-files-folders.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-unicode-dest.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs
  • test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs
  • test/js/node/test/parallel/test-fs-fchown-negative-one.js
  • test/js/node/test/parallel/test-fs-fmap.js
  • test/js/node/test/parallel/test-fs-glob-throw.mjs
  • test/js/node/test/parallel/test-fs-glob.mjs
  • test/js/node/test/parallel/test-fs-internal-assertencoding.js
  • test/js/node/test/parallel/test-fs-lchown-negative-one.js
  • test/js/node/test/parallel/test-fs-long-path.js
  • test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js
  • test/js/node/test/parallel/test-fs-mkdtempDisposableSync.js
  • test/js/node/test/parallel/test-fs-open.js
  • test/js/node/test/parallel/test-fs-opendir.js
  • test/js/node/test/parallel/test-fs-promises-file-handle-pull.js
  • test/js/node/test/parallel/test-fs-promises-file-handle-pullsync.js
  • test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js
  • test/js/node/test/parallel/test-fs-promises-file-handle-writer.js
  • test/js/node/test/parallel/test-fs-promises-mkdtempDisposable.js
  • test/js/node/test/parallel/test-fs-promises-readfile-empty.js
  • test/js/node/test/parallel/test-fs-promises-statfs-validate-path.js
  • test/js/node/test/parallel/test-fs-promises-watch-ignore-function.mjs
  • test/js/node/test/parallel/test-fs-promises-watch-ignore-glob.mjs
  • test/js/node/test/parallel/test-fs-promises-watch-ignore-invalid.mjs
  • test/js/node/test/parallel/test-fs-promises-watch-ignore-mixed.mjs
  • test/js/node/test/parallel/test-fs-promises-watch-ignore-regexp.mjs
  • test/js/node/test/parallel/test-fs-promises-watch-iterator.js
  • test/js/node/test/parallel/test-fs-promises-writefile.js
  • test/js/node/test/parallel/test-fs-read-offset-null.js
  • test/js/node/test/parallel/test-fs-read-stream-encoding.js
  • test/js/node/test/parallel/test-fs-read-stream-err.js
  • test/js/node/test/parallel/test-fs-read-stream-inherit.js
  • test/js/node/test/parallel/test-fs-read-stream-pos.js
  • test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js
  • test/js/node/test/parallel/test-fs-read-stream.js
  • test/js/node/test/parallel/test-fs-read-zero-length.js
  • test/js/node/test/parallel/test-fs-readdir-recursive.js
  • test/js/node/test/parallel/test-fs-readfile-eof.js
  • test/js/node/test/parallel/test-fs-readfile-fd.js
  • test/js/node/test/parallel/test-fs-readfile-pipe-large.js
  • test/js/node/test/parallel/test-fs-readfile-utf8-fast-path.js
  • test/js/node/test/parallel/test-fs-realpath.js
  • test/js/node/test/parallel/test-fs-rmSync-special-char.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-error.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive.js
  • test/js/node/test/parallel/test-fs-rmdir-throws-not-found.js
  • test/js/node/test/parallel/test-fs-rmdir-throws-on-file.js
  • test/js/node/test/parallel/test-fs-stat-abort-test.js
  • test/js/node/test/parallel/test-fs-stat-bigint.js
  • test/js/node/test/parallel/test-fs-stat-date.mjs
  • test/js/node/test/parallel/test-fs-stat-temporal.mjs
  • test/js/node/test/parallel/test-fs-symlink-dir-junction.js
  • test/js/node/test/parallel/test-fs-watch-ignore-function.js
  • test/js/node/test/parallel/test-fs-watch-ignore-glob.js
  • test/js/node/test/parallel/test-fs-watch-ignore-invalid.js
  • test/js/node/test/parallel/test-fs-watch-ignore-mixed.js
  • test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob-subdirectories.js
  • test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob.js
  • test/js/node/test/parallel/test-fs-watch-ignore-recursive-mixed.js
  • test/js/node/test/parallel/test-fs-watch-ignore-recursive-regexp.js
  • test/js/node/test/parallel/test-fs-watch-ignore-regexp.js
  • test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js
  • test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js
  • test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js
  • test/js/node/test/parallel/test-fs-watch-recursive-add-file.js
  • test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js
  • test/js/node/test/parallel/test-fs-watch-recursive-delete.js
  • test/js/node/test/parallel/test-fs-watch-recursive-promise.js
  • test/js/node/test/parallel/test-fs-watch-recursive-symlink.js
  • test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js
  • test/js/node/test/parallel/test-fs-watch-stop-async.js
  • test/js/node/test/parallel/test-fs-watchfile.js
  • test/js/node/test/parallel/test-fs-write-optional-params.js
  • test/js/node/test/parallel/test-fs-write-stream-change-open.js
  • test/js/node/test/parallel/test-fs-write-stream-eagain.mjs
  • test/js/node/test/parallel/test-fs-write-stream-encoding.js
  • test/js/node/test/parallel/test-fs-write-stream-err.js
  • test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js
  • test/js/node/test/parallel/test-fs-write-stream.js
  • test/js/node/test/parallel/test-fs-write-sync-optional-params.js
  • test/js/node/test/parallel/test-fs-writestream-open-write.js
  • test/js/node/test/parallel/test-fs-writesync-crash.js
  • test/js/node/test/sequential/test-fs-opendir-recursive.js
  • test/js/node/test/sequential/test-fs-readdir-recursive.js
  • test/js/node/test/sequential/test-fs-watch.js
  • test/js/node/watch/fs.watch.test.ts
💤 Files with no reviewable changes (7)
  • test/js/node/test/parallel/test-fs-promises-writefile.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js
  • test/js/node/test/parallel/test-fs-rmdir-recursive.js
  • src/jsc/bindings/NodeDirent.cpp

Comment thread src/js/internal/fs/cp-sync.ts
Comment thread src/js/internal/fs/watch.ts Outdated
Comment thread src/js/internal/streams/iter/transform.ts Outdated
Comment thread src/js/node/fs.promises.ts
Comment thread src/js/node/fs.promises.ts
Comment thread src/js/node/test.ts Outdated
Comment thread test/js/node/fs/fs.test.ts Outdated
Comment thread test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs
Comment thread test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs
Comment thread src/js/node/test.ts
Comment thread src/js/node/fs.ts Outdated
Comment thread src/js/node/fs.promises.ts Outdated
Comment thread src/js/node/test.ts Outdated
Comment thread src/js/internal/fs/watch.ts
Comment thread src/js/node/fs.promises.ts
Comment thread test/js/node/test/common/fs.js
Comment thread src/js/node/fs.promises.ts
Comment thread src/js/node/fs.ts Outdated
Comment thread src/sys/sys_uv.rs
Comment thread src/js/node/fs.ts
@cirospaciari
cirospaciari force-pushed the ciro/port-node-fs-tests branch from 7088bad to 2befe5f Compare June 5, 2026 23:11
Comment thread src/js/node/fs.promises.ts Outdated
Comment thread src/js/node/fs.ts Outdated
Comment thread src/js/node/test.ts
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/fs.ts Outdated
@cirospaciari
cirospaciari force-pushed the ciro/port-node-fs-tests branch from dc2fe78 to 43f541b Compare June 6, 2026 01:36
Comment thread src/js/node/worker_threads.ts Outdated
@cirospaciari
cirospaciari force-pushed the ciro/port-node-fs-tests branch from 43f541b to cea21ad Compare June 6, 2026 02:49
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/fs.ts
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/internal/streams/iter/transform.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/fs.promises.ts
Comment thread src/js/node/fs.ts
Comment thread test/js/node/fs/promises.test.js Outdated
@cirospaciari
cirospaciari force-pushed the ciro/port-node-fs-tests branch from 4dd0c03 to a879d95 Compare June 6, 2026 20:05
Comment thread src/js/internal/streams/iter/pull.ts
Comment thread src/js/node/fs.promises.ts
Comment thread test/js/node/worker_threads/worker_threads.test.ts Outdated
Comment thread src/js/node/fs.ts
Comment thread src/js/internal/fs/cp.ts Outdated
cirospaciari added a commit that referenced this pull request Jun 22, 2026
Follow-up to #31830. That PR routed all recursive
`fs.cp`/`fs.cpSync`/`fs.promises.cp` calls through the node-ported JS
walker so relative-symlink rewriting and the `ERR_FS_CP_*` error codes
match node, which meant recursive copies on macOS no longer used a
single whole-tree `clonefile()`. This restores the fast path for the
cases where it is indistinguishable from the walker.

## Summary

- Recursive `fs.cp`/`cpSync`/`promises.cp` go back to the native path
(one `clonefile()` of the whole tree on macOS) when: the options are the
defaults already required for the existing single-file fast path (no
filter/dereference/preserveTimestamps/verbatimSymlinks/mode/errorOnExist,
force), the destination does not exist (so node's merge semantics never
come into play), and a metadata-only readdir scan of the source tree
finds nothing but regular files and directories.
- Anything else still uses the ported walker: symlinks (node rewrites
relative targets against the source tree), FIFOs/sockets/devices
(node-specific `ERR_FS_CP_*` errors), entries whose type the filesystem
does not report, an existing destination, or a scan error. Non-macOS
platforms are completely unchanged — the directory branch still returns
`ok: false` there, and the scan call is dead-code-eliminated.
- The decision lives in `tryNativeFastPathSync`/`tryNativeFastPath` in
`src/js/internal/fs/cp-sync.ts` / `cp.ts`; node's path validation
(`checkPaths`, `checkParentPaths`, EISDIR) still runs before the native
handoff, and the bail-out path keeps reusing the already-computed stats
via `checked` exactly as before.
- Adds `bench/fs-cp/cp.mjs`: recursive copy of a 256-file tree, sync and
promises, with and without a symlink in the tree (the symlink variant
always exercises the walker), runnable under both bun and node.

## Test plan

- [x] New tests in `test/js/node/fs/cp.test.ts` for both `fs.cpSync` and
`fs.promises.cp`, asserting the behaviors the scan must protect:
relative in-tree symlink targets are rewritten to absolute paths
resolved against the source tree (verified against node v26.3.0), file
and directory modes are preserved into a fresh destination, and a FIFO
inside the tree is rejected with `ERR_FS_CP_FIFO_PIPE`. On macOS these
run against trees that bail out of the fast path; the existing cp suite
plus the ported node tests cover the trees that take it.
- [x] `bun bd test test/js/node/fs/cp.test.ts` — 46 pass, 0 fail (Linux
debug build; Linux behavior is unchanged by this PR).
- [x] `bench/fs-cp/cp.mjs` runs under both node 26.3.0 and the debug
build.
- [ ] macOS CI green (the platform where the new branch is actually
taken).
Jarred-Sumner pushed a commit that referenced this pull request Jun 28, 2026
…gh symlinks (#32853)

## What

Fixes two cases where `Bun.Glob.scan()` silently returns an empty result
for patterns that look obviously correct and work in every reference
implementation (bash, picomatch, minimatch, fast-glob):

```js
// 1. Explicitly-named dotfile segment
new Bun.Glob(".dotdir/inner.txt").scanSync(".")
// before: []              (dot:false hides an EXPLICITLY-named dotfile)
// after:  [".dotdir/inner.txt"]

// 2. Literal path segment through a symlinked directory
new Bun.Glob("linkdir/file.txt").scanSync(".")   // linkdir -> realdir
// before: []              (followSymlinks:false blocks even a literal path)
// after:  ["linkdir/file.txt"]
```

Both are silent empty results, not errors. Glob is how people select
files for builds, tests, deploys and uploads, so "no matches" quietly
excludes files. A project that lives behind a symlink (pnpm layouts,
mounted volumes, `/tmp` on macOS) or a config that names a dotfile
explicitly would process nothing.

## Why

**Explicit dotfiles.** `match_pattern_dir` and `match_pattern_impl` in
`GlobWalker.rs` rejected any entry whose name starts with `.` whenever
`dot: false`, without looking at the pattern segment. The ecosystem
convention is that the `dot` option only governs whether *wildcards*
match dotfiles; a segment whose pattern text itself starts with `.` is
an explicit request for that name and matches regardless:

| pattern | candidate | picomatch | minimatch | fast-glob | bash | bun
before | bun after |
|---|---|---|---|---|---|---|---|
| `.dotdir/inner.txt` | `.dotdir/inner.txt` | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| `.*/inner.txt` | `.dotdir/inner.txt` | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| `*/inner.txt` | `.dotdir/inner.txt` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| `**/inner.txt` | `.dotdir/inner.txt` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |

**Literal path through a symlink.** The `SymLink` arm of the directory
iterator only descended when `follow_symlinks` was set. But
`followSymlinks` is documented (and implemented everywhere else) as a
*wildcard-traversal* option: whether `*`/`**` should walk through
symlinked directories. A segment that names the symlink literally is an
explicit path the user wrote; fast-glob resolves it regardless of
`followSymbolicLinks`:

| pattern | fast-glob `followSymbolicLinks:false` | bun
`followSymlinks:false` before | bun after |
|---|---|---|---|
| `linkdir/file.txt` | `["linkdir/file.txt"]` | `[]` |
`["linkdir/file.txt"]` |
| `linkdir/*.txt` | `["linkdir/file.txt"]` | `[]` |
`["linkdir/file.txt"]` |
| `*/file.txt` | `["realdir/file.txt"]` | `["realdir/file.txt"]` |
`["realdir/file.txt"]` |
| `**/file.txt` | `["realdir/file.txt"]` | `["realdir/file.txt"]` |
`["realdir/file.txt"]` |

## How

- `match_pattern_impl`: bypass the dot filter when the pattern
component's own text starts with `.`.
- `match_pattern_dir`: move the dot check after the
`**`-advances-to-next-segment check so `**/.dotdir/...` can advance;
`**` on its own still never descends through a hidden entry.
- `SymLink` entry handling: when `follow_symlinks` is off, compute the
subset of active components that are `SyntaxHint::Literal` and match the
entry name; if non-empty, push the symlink work item with *only* that
subset. Wildcard components stay out of the propagated set, so `*`/`**`
still respect `followSymlinks:false` and cycles reached via wildcards
cannot loop.

## Tests

Added to `test/js/bun/glob/scan.test.ts`: positive cases for each fix
plus negative cases proving wildcards still hide dotfiles, wildcards
still respect `followSymlinks:false`, and a `loop -> .` symlink cycle
reached via `**` under a literally-named parent does not recurse.

```
# before (USE_SYSTEM_BUN=1):  9 fail / 9 pass
# after  (bun bd):            18 pass
# full test/js/bun/glob/scan.test.ts: 190 pass, 0 fail
```

## Not in this PR

Four other `Bun.Glob` divergences from the ecosystem surfaced alongside
these, all in the pattern matcher rather than the walker: single-item
`{a}` braces expanding instead of being literal, unterminated `{` and
`[` not falling back to literal, and `scan()` not honouring the same
backslash escaping as `match()`. Those live in `src/glob/matcher.rs` /
component classification and deserve their own change.

Fixes #28021 (the `Bun.Glob` scanner-level dot suppression described
there; the `fs.glob` symptom was separately sidestepped by the minimatch
port in #31830, but `Bun.Glob` itself still had the bug).

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
cirospaciari added a commit that referenced this pull request Jul 9, 2026
node:test's mock.fn has been implemented since #31830, so the hand-rolled call
counter is no longer needed. Import mock from node:test as upstream does; the
file is now byte-identical to node v26.3.0's copy, which is the point of
vendoring it. fn.mock.calls.length reads the same in both runtimes.

The remaining `// BUN:` note in test-worker-environmentdata.js is not stale:
require('internal/worker') is still unavailable without --expose-internals.
cirospaciari added a commit that referenced this pull request Jul 17, 2026
…ility tests and fix the gaps they surface — ALS run/disable + withScope/defaultValue, http client ALS across reused agent sockets, http2 ALS context, AsyncResource.bind, EventEmitterAsyncResource, timerify (+22 tests) (#31825)

Brings the async-tagged portion of the Node test suite in line with Node
v26.3.0 by porting upstream tests verbatim and fixing the runtime gaps
they expose across `async_hooks`, `events`, `http`, `http2`, and
`perf_hooks`.

| | parallel | sequential | **total** |
|---|---|---|---|
| **async** | 111/169 (65.7%) | 0/2 | **111/171 (64.9%)** |

22 upstream tests added (the rest of the original 56 have since landed
via #31826 / #31830; coverage 32% → 65%; every vendored test passes).

### Behavior changes

- **`AsyncLocalStorage.run` context corruption**: the context slot was
restored by an index captured before the callback, so a `disable()`
during the callback spliced the array and the restore landed on the
wrong store (or resurrected a disabled one). Restore is now by identity,
matching Node's `enterWith(prior)` semantics, including re-adding and
re-enabling a previous value.
- **`AsyncLocalStorage` v26 API surface**: the `defaultValue` and `name`
constructor options, and `withScope()` returning a disposable
`RunScope`.
- **`AsyncResource.bind`** preserved neither the wrapped function's
arity (`fn.length`) nor call-time `this` (it forced the resource as
`thisArg`); both now match Node.
- **`EventEmitterAsyncResource`** was missing the
`asyncId`/`triggerAsyncId`/`asyncResource` getters and the
`asyncResource.eventEmitter` back-reference; rewritten to Node's class
shape (`EventEmitterReferencingAsyncResource`).
- **http client `AsyncLocalStorage` across reused agent sockets**: the
llhttp `HTTPParser` binding ignores the async-resource argument, so a
keep-alive socket reused by a second request dispatched parser callbacks
(and the `'response'`/`'data'`/`'end'` chain) in the first request's ALS
context. `tickOnSocket` now snapshots the active async-context frame on
the request and each socket listener
(`data`/`end`/`error`/`close`/`drain`/`timeout`) runs inside it via
`internal/async_context_frame.run`
(`test-async-local-storage-http-agent`). The frame is captured in
`onSocket()` as well as `tickOnSocket`: `onSocket` attaches the socket's
`error` listener synchronously but `tickOnSocket` only runs a tick
later, and `runInFrame` **installs** the frame it is given rather than
leaving the ambient one alone — so an error arriving in that window used
to run the user's `'error'` handler in the root context and lose the
store, where Node keeps it via the socket's own AsyncWrap.
- **http2 client streams lost the `AsyncLocalStorage` context** active
at `request()` time — `Http2Stream` captures the frame at construction
and native `#Handlers` are wrapped via `withStreamFrame`; the session
captures its own frame so `'close'` doesn't inherit the last stream's.
Both frames are released once their last read is done (stream in
`_destroy`, session after the emit in `emitSessionCloseNT`) so a stream
or session retained past its terminal event does not pin the store — the
http1 counterpart of `closeRequest()`'s cleanup.
- **`perf_hooks`**: `performance.timerify` (and the top-level `timerify`
export), the `'function'` `PerformanceObserver` entry type (JS-side
dispatch wrapping the WebCore observer), and `PerformanceNodeEntry`.
- **`perf_hooks` export surface** now matches v26.3.0's 13 keys:
`PerformanceNodeEntry` is no longer exported (Node names the class but
deliberately does not export it — an earlier revision of this PR did),
and the top-level `eventLoopUtilization` Node exports was missing and is
added. A test pins the surface against the real v26.3.0 key list.
`PerformanceNodeTiming` is knowingly left as a bun-only extra: it
predates this branch and removing a public export is a breaking change
that belongs in its own PR.
- **`run()` on a disabled storage now restores on the way out.** The
restore block was gated on `!wasDisabled`, so `als.disable();
als.run('Y', cb)` left `'Y'` installed after `run()` returned where Node
yields the `defaultValue` (Node's finally is an unconditional
`enterWith(prior)`). The gate dates to #7015, which guarded a
`disable()` *during* the callback by reading the flag at exit;
snapshotting it at entry silently widened it to skip restoration for
storages disabled *before* the call. The was-absent case is already
handled by the identity-relocating branches, so the gate is gone.
- **`perf_hooks` option defaults are pollution-safe.** `timerify()` and
`createHistogram()` defaulted options to a plain `{}`, so a polluted
`Object.prototype.histogram`/`figures` made them throw where Node —
which defaults both to `kEmptyObject` — succeeds. `createHistogram`
additionally took `options || {}`, silently accepting
`null`/`0`/`'x'`/`[]` that Node rejects with `ERR_INVALID_ARG_TYPE`; it
now validates like `timerify` already did.
- **`AsyncLocalStorage` store comparison is SameValue**, matching Node's
primordial `ObjectIs`. Stores were compared with `===`, so `NaN` was
unusable as a store value (`run(NaN)`, `enterWith(NaN)` and `{
defaultValue: NaN }` all tripped the debug restore assertions Node has
no trouble with). `run()`'s unchanged-value short-circuit also read
`Object.is` off the mutable global: userland patching `Object.is` made
`run()` return the wrong store in **release** builds, where Node is
immune. Both now use a pure-operator SameValue helper — a load-time
`Object.is` capture would not fix it, since builtins load lazily and
would inherit a patch applied before the first `require`.
- **`validateObject`**: defer the `isArray` probe (which can throw on a
Proxy) until after the cheap `null`/callable rejections, keeping the
existing `RETURN_IF_EXCEPTION`.
- **test/common**: vendor `common/repl.js`, export `hasQuic` from
`index.mjs`, add test-runner fixtures.

### Test adaptations (commented in-file)

- Stack-overflow tests use non-tail recursion — JSC has proper tail
calls, so upstream's tail-recursive overflow never overflows.
- `test-async-local-storage-weak-asyncwrap-leak` uses a
`FinalizationRegistry` instead of `v8.queryObjects`.
- webcrypto skips the unimplemented ML-KEM/`getPublicKey` methods.

### Known limitations / follow-ups

- ~30 unported tests assert the `createHook`
`init`/`before`/`after`/`destroy` lifecycle, which Bun intentionally
does not implement; the rest need
`internalBinding`/`--expose-internals`, inspector async stack traces,
`--trace-events`, `vm.Module.hasAsyncGraph`, TLS 1.2 legacy session
resumption, the experimental `stream/iter` module, or Node's python
harness.
- 3 repl async tests become portable once `repl.start()` lands (separate
workstream).
- The `fs.cp` and `ClientRequest.destroy(err)` work originally in this
PR has landed separately via #31830 and #31587 and is dropped from this
diff.

### Testing

No performance regression: a release A/B (interleaved across fresh
processes, same native objects, only the JS swapped) puts
`AsyncLocalStorage.run()` at **9.12ns vs 9.75ns** (-6.5%; the
pure-operator SameValue inlines where the `Object.is` host call did not)
and `http.request()` over a keep-alive agent at **52.8µs vs 53.1µs**
(-0.55%, within noise).

All 111 vendored async tests pass with the debug build, plus the unit
suites for every touched module (`async_hooks/`, `events/`,
`perf_hooks/`, `http2/`, `http/`); remaining failures in those suites
reproduce on a clean-main binary (pre-existing).

---------

Co-authored-by: robobun <robobun@oven.sh>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
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