Skip to content

fs: port Node.js fs compatibility tests and fix the gaps they surface — cp error semantics, watcher event delivery, watch ignore+AbortSignal, stream/iter + FileHandle pull/writer, glob port, opendir/Dir, mkdtempDisposable, rmdir-recursive removal (+122 tests)#31830

Open
cirospaciari wants to merge 33 commits into
mainfrom
ciro/port-node-fs-tests

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

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

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

122 verbatim upstream tests added, 39 refreshed to their v26.3.0 content; the full vendored fs suite (332 files) passes 332/332. Excluding the 13 --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).
  • stream/iter (require("stream/iter") / node:stream/iter) ported from node, plus FileHandle.prototype.pull/pullSync/writer and zlib/iter; registered as builtins alongside stream/promises.
  • 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 minimatch vendored inline — 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).
  • 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.
  • 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: true now throws ERR_INVALID_ARG_VALUE like node (the option was removed upstream in v16; use fs.rm). Bun's own tests are migrated to rm/rmSync, and the seven legacy test-fs-rmdir-recursive* vendored tests (removed upstream) are retired.
  • 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), and node:test's mock.fn/mock.method namespace backed by a node-shaped MockFunctionContext.
  • common.isInsideDirWithUnusualChars, common/fs.js, and a --experimental-stream-iter flag branch added to the vendored test harness; ERR_OPERATION_FAILED added to the error-code registry (with the checked-in ErrorCode.rs discriminants regenerated).

Known limitations / follow-ups

  • The 13 remaining unvendored parallel tests need --expose-internals/internal/test/binding; a minimal shim could unlock roughly five of them. 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.
  • zlib/iter transforms buffer input and run the one-shot codec at the flush signal instead of driving the raw zlib binding incrementally (not exposed to builtins); observable round-trip behavior is identical. push(), duplex(), broadcast/share, merge(), and classic-stream interop from stream/iter are not ported (nothing vendored reaches them). Zstd not ported.
  • Pre-existing and unrelated: test/js/node/fs/fs.test.ts intermittently panics Deadlock detected in native AsyncReaddirRecursiveTask::perform_work at module scope (reproduces byte-for-byte on an unmodified baseline binary), and the readdirSync recursive x 100 tests time out under the debug build. The recursive-readdir family needs its own investigation.

Testing

Validated locally with the debug build before pushing:

  • Full vendored fs sweep (test-fs-* parallel + sequential, runner-equivalent semantics with per-test TEST_SERIAL_ID isolation): 332/332 pass.
  • Bun regression gates: fs/cp.test.ts, fs/dir.test.ts, fs/glob.test.ts (corrected 4 expectations against a real node v26.3.0 oracle), node/watch/, bun-object/write.spec.ts, worker_threads/ — 0 failures (one pre-existing worker eval-leak failure reproduced on baseline).
  • 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5123985a-fc8a-497a-8e50-7cce8729751a

📥 Commits

Reviewing files that changed from the base of the PR and between c875b9b and 602f027.

📒 Files selected for processing (16)
  • src/js/internal/fs/glob.ts
  • src/runtime/node/node_fs.rs
  • src/runtime/node/path_watcher.rs
  • src/sys/sys_uv.rs
  • test/fixtures/copy/kitchen-sink/README.md
  • test/fixtures/copy/kitchen-sink/a/b/README2.md
  • test/fixtures/copy/kitchen-sink/a/b/index.js
  • test/fixtures/copy/kitchen-sink/a/c/README2.md
  • test/fixtures/copy/kitchen-sink/a/c/d/README3.md
  • test/fixtures/copy/kitchen-sink/a/c/d/index.js
  • test/fixtures/copy/kitchen-sink/a/c/index.js
  • test/fixtures/copy/kitchen-sink/a/index.js
  • test/fixtures/copy/kitchen-sink/index.js
  • test/js/node/fs/cp.test.ts
  • test/js/node/module/node-module-module.test.js
  • test/js/node/test/parallel/test-permission-fs-supported.js

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
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch ciro/port-node-fs-tests

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator
Updated 3:10 AM PT - Jun 10th, 2026

@robobun, your commit 867aeb4 has 1 failures in Build #61630 (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
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 and others added 26 commits June 10, 2026 00:14
…watch

- glob: sort directory entries before traversal. The ported algorithm's
  seen-cache and "**/.." queueing are sensitive to the order entries are
  visited in, and some orders silently drop results (reproducible in node
  itself by feeding it the same order). Sorting makes traversal
  deterministic across filesystems; fixes a/!(symlink)/**/../* dropping
  a/c on some CI machines
- lchown: stop returning a TODO error on Windows; route through
  uv_fs_lchown, which is a no-op success there like node
- watch: when a recursive watcher attaches to a newly created directory,
  scan it and synthesize rename events for entries that were created
  before the inotify watch existed, like node's recursive watcher
- replace the test/fixtures/copy/kitchen-sink symlink with real files;
  git checks symlinks out as plain text files on Windows CI, which made
  readdir fail with ENOTDIR
- cp.test.ts: normalize separators in the filter assertion (cp resolves
  src so Windows sees backslashes) and expect ERR_FS_CP_EINVAL when
  copying a file onto itself (node behavior; the regression guarded
  against is EBUSY)
- builtinModules count 76 -> 78 (stream/iter and zlib/iter are new)
- permission test: add mkdtempDisposableSync to the supported list,
  matching upstream
node:test mocks:
- implement options.times (revert to the original after N calls) for
  mock.fn/method/getter/setter, with node's validation
- mock.getter/setter: shift an options-object out of the implementation
  slot before spreading the getter/setter flag, so the documented
  (object, methodName, options) overload works; reject getter/setter:
  false like node
- mock.method: reject options.getter together with options.setter
- restoreAll()/reset() now also restores bare mock.fn() mocks to their
  original function (previously only method mocks were tracked)
- exclude null from the implementation->options argument shifts

fs:
- rmdir/rmdirSync/promises.rmdir reject any *defined* recursive option
  (node uses !== undefined, not truthiness) and fs.rmdir validates its
  callback again
- promises.watch validates options.signal before creating the watcher,
  and removes its abort listener on every iterator exit path
- FileHandle.writer: a failed async write/writev poisons the writer
  (like fail()) since the cursor/limit were advanced optimistically
- Dir async iterator closes via the queued close() instead of
  closeSync(), avoiding ERR_DIR_CONCURRENT_OPERATION on early exit
- opendir's synthesized stat-failure message no longer doubles the
  error code prefix
- cp/cp-sync: the EISDIR fast-path errors now carry errno EISDIR
  (was EINVAL), matching node

stream/iter:
- zlib transforms (async variants) now drive node:zlib Transform
  streams incrementally with backpressure instead of buffering all
  input and running a one-shot codec at flush time; sync variants
  still buffer (a synchronous incremental write needs the native
  zlib handle, which builtins don't have)
- drop two no-op type-guard branches left from the primordials port

tests: cover the mock fixes; await two fire-and-forget .rejects
assertions in fs.test.ts
The sync variants no longer buffer the whole input and run a one-shot
codec at flush time. node:zlib's native handle already exposes the same
synchronous chunk API node's binding has (writeSync + the shared
Uint32Array write state), so drive it directly: construct the regular
node:zlib stream object for its validated, initialized handle, then feed
input chunk-by-chunk with the codec's process flag and yield output as
the engine produces it, finalizing with the finish flag on the null
flush signal. This is a direct port of node's makeZlibTransformSync
loop, including the no-copy buffer handoff rules.

Memory now stays bounded for the sync pipelines as well, and output is
emitted mid-stream instead of only at the end. Empty-input semantics are
unchanged: compressors still emit a valid empty container, decompressors
emit nothing rather than erroring on an unfinished stream.
…pullSync lifecycle

- cp/cpSync/promises.cp validate paths like node's getValidatedPath:
  URL instances convert via fileURLToPath, strings and Buffers pass
  through unresolved (no path.resolve, no 'file:'-prefix string
  sniffing), null bytes rejected. Filters now see the caller's relative
  paths like node; Buffer paths get past validation and behave like
  node's JS cp implementation downstream
- watch ignore matchers coerce Buffer filenames (encoding: 'buffer') to
  strings instead of crashing in basename()/Glob.match()
- pullSync acquires its ref when iteration starts (matching pull()) so
  an unconsumed iterable can't hang close(), and cleanup is idempotent
  per iterator so double iteration can't double-unref
- writevAll retries the writev on zero-byte writes instead of falling
  into the concat path with a never-tripping retry counter
- Dir.read(callback) returns undefined like node (and close(cb))
- Windows Syscall::lchown in bun_sys delegates to sys_uv::lchown like
  chown/fchown instead of a second hardcoded no-op
cp no longer resolves its arguments (matching node's getValidatedPath),
so the .path on ERR_FS_EISDIR / ERR_FS_CP_EEXIST is the caller's string
verbatim - on Windows that's the mixed-separator concatenation the test
built, not the join()-normalized form. Verified against node's JS cp
implementation, which echoes the unnormalized caller path the same way.
Same conclusion and fix as the cp tests on the node-v26-async-tests
branch.
- FileHandle.writer() defers its handle ref to the first actual write
  (like pull/pullSync defer it to iteration), so an unused writer can't
  pin the handle and hang close(); end()/fail()/cleanup only unref if
  the ref was taken
- fs.opendir's eager path check (ENOTDIR/ENOENT at open time) now runs
  on an async stat in the callback and promises forms, so the JS thread
  isn't blocked and the callback never fires synchronously; argument
  validation still throws synchronously like node, and opendirSync keeps
  the sync check
- worker_threads: if a transferList item's transfer or the WebWorker
  construction throws after FileHandles were already neutered, restore
  their fds so the handles stay usable instead of orphaning the fds
- node:test mocks: validateBoolean for options.getter/options.setter and
  node's onCall validation in mockImplementationOnce (integer, no
  earlier than the next call)
- cp: thread the stats from the native fast-path probe into the ported
  fallback so the top-level checkPaths/checkParentPaths syscalls don't
  run twice for recursive directory copies with default options

tests: worker transfer restore (sync construct failure and partial
transfer), async opendir callback timing and ENOTDIR-via-callback,
unused writer + close
…ta references

Structured clone keeps a transfer marker shared between graph positions
as one object, but the unpack pass deserialized it once per encounter,
so workerData = { a: fh, b: fh } produced two FileHandle instances
wrapping the same fd - closing one left the other pointing at a closed
(or recycled) descriptor. Replace the cycle-detection Set with a memo
Map so each marker deserializes exactly once, like node's host-object
back-references; containers map to themselves for cycle handling.
…nced ones

- transferList: [fh, fh] now throws DataCloneError like node (and the
  HTML spec) instead of neutering the handle twice, where the second
  kTransfer() read the already-cleared fd and clobbered the real marker;
  the rollback restores the handle so it stays usable after the throw
- a handle in transferList but never referenced from workerData is
  detached like node, but since no marker will deserialize it on the
  worker side, close the orphaned fd instead of leaking it
- fs.opendir's async path invokes the user callback via
  process.nextTick, matching the rest of the file (a throwing callback
  is an uncaughtException, not an unhandledRejection)
…succeeds

The orphan-fd cleanup (for handles in transferList but not referenced
from workerData) ran inside packJSTransferables, before the WebWorker is
constructed. If construction then threw (e.g. non-cloneable workerData),
the rollback re-installed the already-closed fd number into the user's
FileHandle - EBADF on the lucky path, fd-reuse corruption or a double
close on the unlucky one. Node fully restores the handle in this case
(verified: fd intact and readable after the DataCloneError), so defer
the orphan close to a finalize step that the Worker constructor invokes
only after the WebWorker is successfully created; the rollback then
always finds the fd still open.
Structured clone walks Map and Set entries (keys included), so a
transferred handle inside one is reachable on the receive side - but the
pack/unpack walkers only recursed into arrays and plain objects. A
FileHandle inside a Map was neutered by the transfer loop, never
replaced by its marker, treated as unreferenced (fd closed), and the
worker got a dead clone. Walk Map and Set in both directions like the
serializer does, with the same memo/cycle handling.

Also: use the $isArray intrinsic in the two walkers that still used
Array.isArray (consistent with the rest of the block), and drain the
zlib transform pending queues by array swap instead of repeated shift()
(O(N) instead of O(N^2) for high-ratio decompression bursts).
- writer()/pull()/pullSync() capture the fd when created, and since the
  deferred-ref change an unused source no longer blocks close() - so a
  source used after close() would issue I/O on a closed (or recycled)
  descriptor. All entry points now check handle's fd first and throw or
  reject ERR_INVALID_STATE (async write/writev reject, sync variants and
  iteration throw)
- fs.rm and fs.promises.rm now run the same JS-side lstat ->
  ERR_FS_EISDIR validation rmSync got, so all three forms report node's
  error shape for non-recursive directory removal (the callback form
  routes through promises.rm)
…n tests

Three .rejects.toMatchObject assertions were fire-and-forget, so a
non-rejecting promise would have surfaced as a late unhandled rejection
instead of a test failure (and the rm one raced the cleanup that
follows). Same fix as the earlier fs.test.ts instance of this pattern.
- pull's pre-aborted-signal source is now an explicit single-shot
  rejecting iterator instead of a generator that never yields
- drop a no-op ("use strict") expression and the unused
  validateFunction import left in the vendored-minimatch wrapper
- use endsWith instead of a dollar-anchored regex in the vendored brace
  expansion (verified identical ${...}-literal semantics against node)
…ck leak

- pipeToSync treated writeSync/writevSync returning false as a
  successful write; the FileHandle writer returns false to mean 'fall
  back to async write()', which a sync pipe doesn't have - so oversized
  chunks (or a swallowed first-write failure) were silently counted and
  dropped. A refused writevSync now falls back to per-chunk writes (the
  batch total can exceed the sync threshold even when each chunk fits),
  and a refused single chunk throws instead of lying about the byte count
- fh.pull() with an already-aborted signal returns a rejecting iterator
  without locking the handle: with transforms, the pipeline's pre-abort
  branch never consumes the source, so the unlock in its finally never
  ran and the handle stayed locked forever (autoClose is still honored
  on first next, matching the old no-transform behavior)
- worker test helper uses harness tmpdirSync instead of a local
  mkdtempSync wrapper
…in onLink

- the withFileTypes exclude path in #addSubpattern stat'ed the relative
  path: the stat cache is keyed by absolute paths so the lookup always
  missed, and the fallback lstat resolved against process.cwd() instead
  of options.cwd - returning null and silently skipping the user's
  exclude callback whenever cwd differed. Use the fullpath like the
  sibling lookups two lines up (upstream node passes the relative path
  here too). Regression test proves the old code skipped the callback
- cp onLink stat'ed src twice on the dest-exists path; hoist the single
  stat and reuse it for both directory-gated checks (a dangling src
  symlink still surfaces ENOENT, as before)
…essage

- FileHandle[kUnref] routed the deferred close through this.close(),
  which short-circuits on the already-cleared kFd and resolved the
  pending close() promise without ever closing the descriptor - every
  close()-while-an-op-is-in-flight leaked the fd. Close the captured fd
  directly. Regression test proves the old path leaked
- the Dir constructor rejects non-object/string options with
  ERR_INVALID_ARG_TYPE like node (found while verifying opendir option
  handling against node: fsPromises.opendir(dir, 42) now rejects
  instead of resolving)
- pipeToSync's refusal error uses a generic message: writevSync also
  returns false for an exhausted limit or a failed first write, where
  'increase chunkSize' was misleading
- the worker orphan-fd test accepts EBADF or a recycled descriptor
  (another thread can reuse the number before the assertion runs)
…tring options

- fail() and the async end()->cleanup() path released the writer's ref
  (and with autoClose, closed the handle) without checking asyncPending,
  so a write still running on the threadpool could hit a closed or
  recycled fd - endSync() already guarded this. Both paths now defer the
  unref/close to the write's finally via a teardown hook; end() resolves
  with the full byte count once the pending write lands. Regression test
  covers fail()-during-write (write completes, no EBADF, handle closed
  after) and end()-during-write
- the Dir constructor normalizes a string options argument to
  { encoding } like node's getOptions, so opendirSync(path, 'latin1')
  actually applies the encoding and an invalid shorthand throws
  ERR_INVALID_ARG_VALUE (encoding: 'buffer' dirents remain a
  pre-existing native readdir gap, present in releases)
- asyncPending was a boolean, so with two unawaited write() calls the
  first one to finish ran the deferred teardown (unref + autoClose) while
  the second was still on the threadpool against the closed fd - the
  exact race the teardown hook exists to prevent, reintroduced for the
  concurrent case. It's a counter now; teardown runs when the last
  in-flight write lands. Regression test: fail() with two pending writes,
  both complete, no EBADF
- FSWatcher.ref()/unref() go through this._handle (honouring a replaced
  handle) and return this for chaining, like node; the FSEvent
  ref/unref delegates are live again
On Windows the native readdir always emits UTF-8 names (fs.readdirSync
ignores the encoding option there as well - a pre-existing gap), so the
latin1 byte-reinterpretation half of the opendir string-shorthand test
is POSIX-only now; the invalid-shorthand validation half runs
everywhere.
…alidation dedup

- ERR_FS_CP_EEXIST and ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY join the
  ErrorCode registry (appended, so the committed ErrorCode.rs mirror is
  updated append-only) and their helpers construct through the $ERR_*
  intrinsics like the six sibling cp error codes
- rmSync and fs.promises.rm build their ERR_FS_EISDIR through cp-sync's
  fsEisdirError, so the error is a SystemError with node's exact
  message/info shape instead of a bare Error with a hand-assigned code
- the JSTransferable array walkers skip holes so sparse workerData
  arrays stay sparse across the clone like node; documented why the
  transfer marker is an in-band string key (not a privilege boundary)
- validateCpOptions brands its normalized result so the callback
  fs.cp -> promises.cp delegation doesn't re-validate the same object
… options

- restoreAll() no longer disassociates mocks from the tracker - only
  reset() clears the registry, like node ('Unlike mock.reset(),
  mock.restoreAll() does not disassociate the mocks')
- restore() now mirrors node exactly (verified by probing): a method
  mock reinstalls the original descriptor but the context keeps its
  implementation, a bare fn mock reverts to the original, queued
  once-implementations survive, and restore() is re-runnable so a
  still-tracked context can be restored again by a later reset()
- mock.method validates a non-object options argument with
  ERR_INVALID_ARG_TYPE like mock.fn already did

tests mirror the probed node outputs (restoreAll/reset association
lifecycle, surviving once-implementations, options validation)
…ia registry

- trackMockCall pushed the call record before invoking, so a reentrant
  implementation saw callCount() === N+1 where node reports N, recursive
  calls recorded in start order, and the stack was captured pre-invoke.
  The record is pushed in a finally after the implementation runs, like
  node's proxy handlers (verified against node: callCount() inside the
  impl is 0). Test added
- ERR_DIR_CONCURRENT_OPERATION joins the ErrorCode registry (appended;
  ErrorCode.rs mirror updated append-only) and Dir's helper constructs
  through the $ERR_* intrinsic like the sibling ERR_DIR_CLOSED; the
  observable shape (name 'Error', same code/message) matches node
@cirospaciari cirospaciari force-pushed the ciro/port-node-fs-tests branch from e82d207 to f5d20e0 Compare June 10, 2026 00:18
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