diff --git a/src/js/internal/fs/cp-sync.ts b/src/js/internal/fs/cp-sync.ts index bc716a53dd9a..731fb1513285 100644 --- a/src/js/internal/fs/cp-sync.ts +++ b/src/js/internal/fs/cp-sync.ts @@ -1,11 +1,145 @@ // Taken and modified from node.js: https://github.com/nodejs/node/blob/main/lib/internal/fs/cp/cp-sync.js +// Also hosts the option validation and SystemError construction shared with +// internal/fs/cp (async) and the fs.cpSync/fs.cp/fs.promises.cp dispatchers, +// ported from node lib/internal/fs/utils.js and lib/internal/errors.js. +const { validateObject, validateBoolean, validateFunction, validateInteger } = require("internal/validators"); +const { + chmodSync, + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readlinkSync, + statSync, + symlinkSync, + unlinkSync, + utimesSync, +} = require("node:fs"); +const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path"); -// const { EEXIST, EISDIR, EINVAL, ENOTDIR } = $processBindingConstants.os.errno; +const { EEXIST, EISDIR, EINVAL, ENOTDIR } = $processBindingConstants.os.errno; const ArrayPrototypeEvery = Array.prototype.every; const ArrayPrototypeFilter = Array.prototype.filter; const StringPrototypeSplit = String.prototype.split; +// COPYFILE_EXCL | COPYFILE_FICLONE | COPYFILE_FICLONE_FORCE +const kMaxCopyMode = 7; + +const defaultCpOptions = { + dereference: false, + errorOnExist: false, + filter: undefined, + force: true, + preserveTimestamps: false, + recursive: false, + verbatimSymlinks: false, + mode: 0, +}; + +function decorateSystemError(err, prefix, context) { + let message = `${prefix}: ${context.syscall} returned ${context.code} (${context.message})`; + if (context.path !== undefined) message += ` ${context.path}`; + if (context.dest !== undefined) message += ` => ${context.dest}`; + err.message = message; + err.name = "SystemError"; + err.info = context; + err.errno = context.errno; + err.syscall = context.syscall; + if (context.path !== undefined) err.path = context.path; + if (context.dest !== undefined) err.dest = context.dest; + return err; +} + +function fsCpDirToNonDirError(context) { + return decorateSystemError( + $ERR_FS_CP_DIR_TO_NON_DIR(context.message), + "Cannot overwrite non-directory with directory", + context, + ); +} + +function fsCpEExistError(context) { + return decorateSystemError($ERR_FS_CP_EEXIST(context.message), "Target already exists", context); +} + +function fsCpEinvalError(context) { + return decorateSystemError($ERR_FS_CP_EINVAL(context.message), "Invalid src or dest", context); +} + +function fsCpFifoPipeError(context) { + return decorateSystemError($ERR_FS_CP_FIFO_PIPE(context.message), "Cannot copy a FIFO pipe", context); +} + +function fsCpNonDirToDirError(context) { + return decorateSystemError( + $ERR_FS_CP_NON_DIR_TO_DIR(context.message), + "Cannot overwrite directory with non-directory", + context, + ); +} + +function fsCpSocketError(context) { + return decorateSystemError($ERR_FS_CP_SOCKET(context.message), "Cannot copy a socket file", context); +} + +function fsCpSymlinkToSubdirectoryError(context) { + return decorateSystemError( + $ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(context.message), + "Cannot overwrite symlink in subdirectory of self", + context, + ); +} + +function fsCpUnknownError(context) { + return decorateSystemError($ERR_FS_CP_UNKNOWN(context.message), "Cannot copy an unknown file type", context); +} + +function fsEisdirError(context) { + return decorateSystemError($ERR_FS_EISDIR(context.message), "Path is a directory", context); +} + +function getValidMode(mode) { + if (mode == null) { + return 0; + } + validateInteger(mode, "mode", 0, kMaxCopyMode); + return mode; +} + +const kValidatedCpOptions = Symbol("kValidatedCpOptions"); + +function validateCpOptions(options) { + // Callback fs.cp validates before delegating to fs.promises.cp; the brand + // lets the second pass skip re-validating the same object. + if (options?.[kValidatedCpOptions]) return options; + if (options === undefined) { + options = { ...defaultCpOptions }; + options[kValidatedCpOptions] = true; + return options; + } + validateObject(options, "options"); + options = { ...defaultCpOptions, ...options }; + validateBoolean(options.dereference, "options.dereference"); + validateBoolean(options.errorOnExist, "options.errorOnExist"); + validateBoolean(options.force, "options.force"); + validateBoolean(options.preserveTimestamps, "options.preserveTimestamps"); + validateBoolean(options.recursive, "options.recursive"); + validateBoolean(options.verbatimSymlinks, "options.verbatimSymlinks"); + options.mode = getValidMode(options.mode); + if (options.dereference === true && options.verbatimSymlinks === true) { + throw $ERR_INCOMPATIBLE_OPTION_PAIR( + 'Option "dereference" cannot be used in combination with option "verbatimSymlinks"', + ); + } + if (options.filter !== undefined) { + validateFunction(options.filter, "options.filter"); + } + options[kValidatedCpOptions] = true; + return options; +} + function areIdentical(srcStat, destStat) { return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev; } @@ -13,59 +147,19 @@ function areIdentical(srcStat, destStat) { const normalizePathToArray = path => ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(path), sep), Boolean); +// Return true if dest is a subdir of src, otherwise false. +// It only checks the path strings. function isSrcSubdir(src, dest) { const srcArr = normalizePathToArray(src); const destArr = normalizePathToArray(dest); return ArrayPrototypeEvery.$call(srcArr, (cur, i) => destArr[i] === cur); } -// const { codes } = require("internal/errors"); -// const { -// ERR_FS_CP_DIR_TO_NON_DIR, -// ERR_FS_CP_EEXIST, -// ERR_FS_CP_EINVAL, -// ERR_FS_CP_FIFO_PIPE, -// ERR_FS_CP_NON_DIR_TO_DIR, -// ERR_FS_CP_SOCKET, -// ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, -// ERR_FS_CP_UNKNOWN, -// ERR_FS_EISDIR, -// ERR_INVALID_RETURN_VALUE, -// } = codes; -const { - chmodSync, - copyFileSync, - existsSync, - lstatSync, - mkdirSync, - // opendirSync, - readdirSync, - readlinkSync, - statSync, - symlinkSync, - unlinkSync, - utimesSync, -} = require("node:fs"); -const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path"); - -function cpSyncFn(src, dest, opts) { - // Warn about using preserveTimestamps on 32-bit node - // if (opts.preserveTimestamps && process.arch === "ia32") { - // const warning = "Using the preserveTimestamps option in 32-bit " + "node is not recommended"; - // process.emitWarning(warning, "TimestampPrecisionWarning"); - // } - const { srcStat, destStat, skipped } = checkPathsSync(src, dest, opts); - if (skipped) return; - checkParentPathsSync(src, srcStat, dest); - return checkParentDir(destStat, src, dest, opts); -} - function checkPathsSync(src, dest, opts) { if (opts.filter) { const shouldCopy = opts.filter(src, dest); if ($isPromise(shouldCopy)) { - // throw new ERR_INVALID_RETURN_VALUE("boolean", "filter", shouldCopy); - throw new Error("Expected a boolean from the filter function, but got a promise. Use `fs.promises.cp` instead."); + throw $ERR_INVALID_RETURN_VALUE("boolean", "filter", shouldCopy); } if (!shouldCopy) return { __proto__: null, skipped: true }; } @@ -73,46 +167,42 @@ function checkPathsSync(src, dest, opts) { if (destStat) { if (areIdentical(srcStat, destStat)) { - // throw new ERR_FS_CP_EINVAL({ - // message: "src and dest cannot be the same", - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error("src and dest cannot be the same"); + throw fsCpEinvalError({ + message: "src and dest cannot be the same", + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } if (srcStat.isDirectory() && !destStat.isDirectory()) { - // throw new ERR_FS_CP_DIR_TO_NON_DIR({ - // message: `cannot overwrite directory ${src} ` + `with non-directory ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EISDIR, - // code: "EISDIR", - // }); - throw new Error(`cannot overwrite directory ${src} with non-directory ${dest}`); + throw fsCpDirToNonDirError({ + message: `cannot overwrite non-directory ${dest} with directory ${src}`, + path: dest, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); } if (!srcStat.isDirectory() && destStat.isDirectory()) { - // throw new ERR_FS_CP_NON_DIR_TO_DIR({ - // message: `cannot overwrite non-directory ${src} ` + `with directory ${dest}`, - // path: dest, - // syscall: "cp", - // errno: ENOTDIR, - // code: "ENOTDIR", - // }); - throw new Error(`cannot overwrite non-directory ${src} with directory ${dest}`); + throw fsCpNonDirToDirError({ + message: `cannot overwrite directory ${dest} with non-directory ${src}`, + path: dest, + syscall: "cp", + errno: ENOTDIR, + code: "ENOTDIR", + }); } } if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${src} to a subdirectory of self ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`); + throw fsCpEinvalError({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return { __proto__: null, srcStat, destStat, skipped: false }; } @@ -144,18 +234,48 @@ function checkParentPathsSync(src, srcStat, dest) { throw err; } if (areIdentical(srcStat, destStat)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${src} to a subdirectory of self ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`); + throw fsCpEinvalError({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return checkParentPathsSync(src, srcStat, destParent); } +// node-correct validation before handing off to the native fast path +// (which performs the copy but does not implement node's cp error codes). +function tryNativeFastPathSync(src, dest, opts) { + const checked = checkPathsSync(src, dest, opts); + const { srcStat, destStat } = checked; + checkParentPathsSync(src, srcStat, dest); + if (srcStat.isDirectory() && !opts.recursive) { + throw fsEisdirError({ + message: `${src} is a directory (not copied)`, + path: src, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); + } + // The native copy is only node-equivalent for regular-file -> regular-file + // (or missing dest). Symlinks (node resolves relative link targets), + // directories (may contain symlinks), and special files (node-specific + // error codes) must go through the ported implementation. + return { ok: srcStat.isFile() && (!destStat || destStat.isFile()), checked }; +} + +function cpSyncFn(src, dest, opts, checked?) { + // `checked` carries the stats from a preceding tryNativeFastPathSync so the + // fallback doesn't re-run the same checkPaths/checkParentPaths syscalls. + const { srcStat, destStat, skipped } = checked ?? checkPathsSync(src, dest, opts); + if (skipped) return; + if (checked === undefined) checkParentPathsSync(src, srcStat, dest); + return checkParentDir(destStat, src, dest, opts); +} + function checkParentDir(destStat, src, dest, opts) { const destParent = dirname(dest); if (!existsSync(destParent)) mkdirSync(destParent, { recursive: true }); @@ -169,45 +289,41 @@ function getStats(destStat, src, dest, opts) { if (srcStat.isDirectory() && opts.recursive) { return onDir(srcStat, destStat, src, dest, opts); } else if (srcStat.isDirectory()) { - // throw new ERR_FS_EISDIR({ - // message: `${src} is a directory (not copied)`, - // path: src, - // syscall: "cp", - // errno: EINVAL, - // code: "EISDIR", - // }); - throw new Error(`${src} is a directory (not copied)`); + throw fsEisdirError({ + message: `${src} is a directory (not copied)`, + path: src, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); } else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) { return onFile(srcStat, destStat, src, dest, opts); } else if (srcStat.isSymbolicLink()) { return onLink(destStat, src, dest, opts); } else if (srcStat.isSocket()) { - // throw new ERR_FS_CP_SOCKET({ - // message: `cannot copy a socket file: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy a socket file: ${dest}`); + throw fsCpSocketError({ + message: `cannot copy a socket file: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } else if (srcStat.isFIFO()) { - // throw new ERR_FS_CP_FIFO_PIPE({ - // message: `cannot copy a FIFO pipe: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy a FIFO pipe: ${dest}`); + throw fsCpFifoPipeError({ + message: `cannot copy a FIFO pipe: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } - // throw new ERR_FS_CP_UNKNOWN({ - // message: `cannot copy an unknown file type: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy an unknown file type: ${dest}`); + throw fsCpUnknownError({ + message: `cannot copy an unknown file type: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } function onFile(srcStat, destStat, src, dest, opts) { @@ -220,14 +336,13 @@ function mayCopyFile(srcStat, src, dest, opts) { unlinkSync(dest); return copyFile(srcStat, src, dest, opts); } else if (opts.errorOnExist) { - // throw new ERR_FS_CP_EEXIST({ - // message: `${dest} already exists`, - // path: dest, - // syscall: "cp", - // errno: EEXIST, - // code: "EEXIST", - // }); - throw new Error(`${dest} already exists`); + throw fsCpEExistError({ + message: `${dest} already exists`, + path: dest, + syscall: "cp", + errno: EEXIST, + code: "EEXIST", + }); } } @@ -267,6 +382,15 @@ function setDestTimestamps(src, dest) { function onDir(srcStat, destStat, src, dest, opts) { if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); + if (opts.errorOnExist && !opts.force) { + throw fsCpEExistError({ + message: `${dest} already exists`, + path: dest, + syscall: "cp", + errno: EEXIST, + code: "EEXIST", + }); + } return copyDir(src, dest, opts); } @@ -277,19 +401,6 @@ function mkDirAndCopy(srcMode, src, dest, opts) { } function copyDir(src, dest, opts) { - // const dir = opendirSync(src); - // try { - // let dirent; - // while ((dirent = dir.readSync()) !== null) { - // const { name } = dirent; - // const srcItem = join(src, name); - // const destItem = join(dest, name); - // const { destStat, skipped } = checkPathsSync(srcItem, destItem, opts); - // if (!skipped) getStats(destStat, srcItem, destItem, opts); - // } - // } finally { - // dir.closeSync(); - // } for (const dirent of readdirSync(src, { withFileTypes: true })) { const { name } = dirent; const srcItem = join(src, name); @@ -322,28 +433,30 @@ function onLink(destStat, src, dest, opts) { if (!isAbsolute(resolvedDest)) { resolvedDest = resolve(dirname(dest), resolvedDest); } - if (isSrcSubdir(resolvedSrc, resolvedDest)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + `${resolvedDest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`); + let srcIsDir = false; + try { + srcIsDir = statSync(src).isDirectory(); + } catch {} + if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { + throw fsCpEinvalError({ + message: `cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } // Prevent copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { - // throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ - // message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`); + throw fsCpSymlinkToSubdirectoryError({ + message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return copyLink(resolvedSrc, dest); } @@ -353,4 +466,20 @@ function copyLink(resolvedSrc, dest) { return symlinkSync(resolvedSrc, dest); } -export default cpSyncFn; +export default { + cpSyncFn, + validateCpOptions, + tryNativeFastPathSync, + errno: { EEXIST, EISDIR, EINVAL, ENOTDIR }, + fsCpDirToNonDirError, + fsCpEExistError, + fsCpEinvalError, + fsCpFifoPipeError, + fsCpNonDirToDirError, + fsCpSocketError, + fsCpSymlinkToSubdirectoryError, + fsCpUnknownError, + fsEisdirError, + areIdentical, + isSrcSubdir, +}; diff --git a/src/js/internal/fs/cp.ts b/src/js/internal/fs/cp.ts index a58d74cd3bda..b3ffa5f3f20c 100644 --- a/src/js/internal/fs/cp.ts +++ b/src/js/internal/fs/cp.ts @@ -1,35 +1,24 @@ // Taken and modified from node.js: https://github.com/nodejs/node/blob/main/lib/internal/fs/cp/cp.js +const { + errno: { EEXIST, EINVAL, EISDIR, ENOTDIR }, + fsCpDirToNonDirError, + fsCpEExistError, + fsCpEinvalError, + fsCpFifoPipeError, + fsCpNonDirToDirError, + fsCpSocketError, + fsCpSymlinkToSubdirectoryError, + fsCpUnknownError, + fsEisdirError, + areIdentical, + isSrcSubdir, +} = require("internal/fs/cp-sync"); -// const { -// codes: { -// ERR_FS_CP_DIR_TO_NON_DIR, -// ERR_FS_CP_EEXIST, -// ERR_FS_CP_EINVAL, -// ERR_FS_CP_FIFO_PIPE, -// ERR_FS_CP_NON_DIR_TO_DIR, -// ERR_FS_CP_SOCKET, -// ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, -// ERR_FS_CP_UNKNOWN, -// ERR_FS_EISDIR, -// }, -// } = require("internal/errors"); -// const { EEXIST, EISDIR, EINVAL, ENOTDIR } = $processBindingConstants.os.errno; const { chmod, copyFile, lstat, mkdir, opendir, readlink, stat, symlink, unlink, utimes } = require("node:fs/promises"); -const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path"); +const { dirname, isAbsolute, join, parse, resolve } = require("node:path"); const PromisePrototypeThen = $Promise.prototype.$then; const PromiseReject = Promise.$reject; -const ArrayPrototypeFilter = Array.prototype.filter; -const StringPrototypeSplit = String.prototype.split; -const ArrayPrototypeEvery = Array.prototype.every; - -async function cpFn(src, dest, opts) { - const stats = await checkPaths(src, dest, opts); - const { srcStat, destStat, skipped } = stats; - if (skipped) return; - await checkParentPaths(src, srcStat, dest); - return checkParentDir(destStat, src, dest, opts); -} async function checkPaths(src, dest, opts) { if (opts.filter && !(await opts.filter(src, dest))) { @@ -38,47 +27,46 @@ async function checkPaths(src, dest, opts) { const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts); if (destStat) { if (areIdentical(srcStat, destStat)) { - throw new Error("Source and destination must not be the same."); + throw fsCpEinvalError({ + message: "src and dest cannot be the same", + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } if (srcStat.isDirectory() && !destStat.isDirectory()) { - // throw new ERR_FS_CP_DIR_TO_NON_DIR({ - // message: `cannot overwrite directory ${src} with non-directory ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EISDIR, - // code: "EISDIR", - // }); - throw new Error(`cannot overwrite directory ${src} with non-directory ${dest}`); + throw fsCpDirToNonDirError({ + message: `cannot overwrite non-directory ${dest} with directory ${src}`, + path: dest, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); } if (!srcStat.isDirectory() && destStat.isDirectory()) { - // throw new ERR_FS_CP_NON_DIR_TO_DIR({ - // message: `cannot overwrite non-directory ${src} with directory ${dest}`, - // path: dest, - // syscall: "cp", - // errno: ENOTDIR, - // code: "ENOTDIR", - // }); - throw new Error(`cannot overwrite non-directory ${src} with directory ${dest}`); + throw fsCpNonDirToDirError({ + message: `cannot overwrite directory ${dest} with non-directory ${src}`, + path: dest, + syscall: "cp", + errno: ENOTDIR, + code: "ENOTDIR", + }); } } if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${src} to a subdirectory of self ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`); + throw fsCpEinvalError({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return { __proto__: null, srcStat, destStat, skipped: false }; } -function areIdentical(srcStat, destStat) { - return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev; -} - function getStats(src, dest, opts) { const statFunc = opts.dereference ? file => stat(file, { bigint: true }) : file => lstat(file, { bigint: true }); return Promise.all([ @@ -90,22 +78,6 @@ function getStats(src, dest, opts) { ]); } -async function checkParentDir(destStat, src, dest, opts) { - const destParent = dirname(dest); - const dirExists = await pathExists(destParent); - if (dirExists) return getStatsForCopy(destStat, src, dest, opts); - await mkdir(destParent, { recursive: true }); - return getStatsForCopy(destStat, src, dest, opts); -} - -function pathExists(dest) { - return PromisePrototypeThen.$call( - stat(dest), - () => true, - err => (err.code === "ENOENT" ? false : PromiseReject(err)), - ); -} - // Recursively check if dest parent is a subdirectory of src. // It works for all file types including symlinks since it // checks the src and dest inodes. It starts from the deepest @@ -124,27 +96,64 @@ async function checkParentPaths(src, srcStat, dest) { throw err; } if (areIdentical(srcStat, destStat)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${src} to a subdirectory of self ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`); + throw fsCpEinvalError({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return checkParentPaths(src, srcStat, destParent); } -const normalizePathToArray = path => - ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(path), sep), Boolean); +// node-correct validation before handing off to the native fast path +// (which performs the copy but does not implement node's cp error codes). +async function tryNativeFastPath(src, dest, opts) { + const checked = await checkPaths(src, dest, opts); + const { srcStat, destStat } = checked; + await checkParentPaths(src, srcStat, dest); + if (srcStat.isDirectory() && !opts.recursive) { + throw fsEisdirError({ + message: `${src} is a directory (not copied)`, + path: src, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); + } + // The native copy is only node-equivalent for regular-file -> regular-file + // (or missing dest). Symlinks (node resolves relative link targets), + // directories (may contain symlinks), and special files (node-specific + // error codes) must go through the ported implementation. + return { ok: srcStat.isFile() && (!destStat || destStat.isFile()), checked }; +} -// Return true if dest is a subdir of src, otherwise false. -// It only checks the path strings. -function isSrcSubdir(src, dest) { - const srcArr = normalizePathToArray(src); - const destArr = normalizePathToArray(dest); - return ArrayPrototypeEvery.$call(srcArr, (cur, i) => destArr[i] === cur); +async function cpFn(src, dest, opts, checked?) { + // `checked` carries the stats from a preceding tryNativeFastPath so the + // fallback doesn't re-run the same checkPaths/checkParentPaths syscalls. + const { srcStat, destStat, skipped } = checked ?? (await checkPaths(src, dest, opts)); + if (skipped) return; + if (checked === undefined) await checkParentPaths(src, srcStat, dest); + return checkParentDir(destStat, src, dest, opts); +} + +async function checkParentDir(destStat, src, dest, opts) { + const destParent = dirname(dest); + const dirExists = await pathExists(destParent); + if (dirExists) return getStatsForCopy(destStat, src, dest, opts); + await mkdir(destParent, { recursive: true }); + return getStatsForCopy(destStat, src, dest, opts); +} + +function pathExistsFulfilled() { + return true; +} +function pathExistsRejected(err) { + return err.code === "ENOENT" ? false : PromiseReject(err); +} +function pathExists(dest) { + return PromisePrototypeThen.$call(stat(dest), pathExistsFulfilled, pathExistsRejected); } async function getStatsForCopy(destStat, src, dest, opts) { @@ -153,45 +162,41 @@ async function getStatsForCopy(destStat, src, dest, opts) { if (srcStat.isDirectory() && opts.recursive) { return onDir(srcStat, destStat, src, dest, opts); } else if (srcStat.isDirectory()) { - // throw new ERR_FS_EISDIR({ - // message: `${src} is a directory (not copied)`, - // path: src, - // syscall: "cp", - // errno: EISDIR, - // code: "EISDIR", - // }); - throw new Error(`${src} is a directory (not copied)`); + throw fsEisdirError({ + message: `${src} is a directory (not copied)`, + path: src, + syscall: "cp", + errno: EISDIR, + code: "EISDIR", + }); } else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) { return onFile(srcStat, destStat, src, dest, opts); } else if (srcStat.isSymbolicLink()) { return onLink(destStat, src, dest, opts); } else if (srcStat.isSocket()) { - // throw new ERR_FS_CP_SOCKET({ - // message: `cannot copy a socket file: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy a socket file: ${dest}`); + throw fsCpSocketError({ + message: `cannot copy a socket file: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } else if (srcStat.isFIFO()) { - // throw new ERR_FS_CP_FIFO_PIPE({ - // message: `cannot copy a FIFO pipe: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy a FIFO pipe: ${dest}`); + throw fsCpFifoPipeError({ + message: `cannot copy a FIFO pipe: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } - // throw new ERR_FS_CP_UNKNOWN({ - // message: `cannot copy an unknown file type: ${dest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy an unknown file type: ${dest}`); + throw fsCpUnknownError({ + message: `cannot copy an unknown file type: ${dest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } function onFile(srcStat, destStat, src, dest, opts) { @@ -204,14 +209,13 @@ async function mayCopyFile(srcStat, src, dest, opts) { await unlink(dest); return _copyFile(srcStat, src, dest, opts); } else if (opts.errorOnExist) { - // throw new ERR_FS_CP_EEXIST({ - // message: `${dest} already exists`, - // path: dest, - // syscall: "cp", - // errno: EEXIST, - // code: "EEXIST", - // }); - throw new Error(`${dest} already exists`); + throw fsCpEExistError({ + message: `${dest} already exists`, + path: dest, + syscall: "cp", + errno: EEXIST, + code: "EEXIST", + }); } } @@ -261,6 +265,15 @@ async function setDestTimestamps(src, dest) { function onDir(srcStat, destStat, src, dest, opts) { if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); + if (opts.errorOnExist && !opts.force) { + throw fsCpEExistError({ + message: `${dest} already exists`, + path: dest, + syscall: "cp", + errno: EEXIST, + code: "EEXIST", + }); + } return copyDir(src, dest, opts); } @@ -304,29 +317,30 @@ async function onLink(destStat, src, dest, opts) { if (!isAbsolute(resolvedDest)) { resolvedDest = resolve(dirname(dest), resolvedDest); } - if (isSrcSubdir(resolvedSrc, resolvedDest)) { - // throw new ERR_FS_CP_EINVAL({ - // message: `cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`); + // stat(src) follows the link; a dangling src symlink throws ENOENT here, + // same as before (both gated checks below only apply to directories). + const srcStat = await stat(src); + const srcIsDir = srcStat.isDirectory(); + if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { + throw fsCpEinvalError({ + message: `cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } // Do not copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - const srcStat = await stat(src); - if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { - // throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ - // message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, - // path: dest, - // syscall: "cp", - // errno: EINVAL, - // code: "EINVAL", - // }); - throw new Error(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`); + if (srcIsDir && isSrcSubdir(resolvedDest, resolvedSrc)) { + throw fsCpSymlinkToSubdirectoryError({ + message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, + path: dest, + syscall: "cp", + errno: EINVAL, + code: "EINVAL", + }); } return copyLink(resolvedSrc, dest); } @@ -336,4 +350,4 @@ async function copyLink(resolvedSrc, dest) { return symlink(resolvedSrc, dest); } -export default cpFn; +export default { cpFn, tryNativeFastPath }; diff --git a/src/js/internal/fs/glob.ts b/src/js/internal/fs/glob.ts index a7483f788229..a7bd5301351b 100644 --- a/src/js/internal/fs/glob.ts +++ b/src/js/internal/fs/glob.ts @@ -1,109 +1,2974 @@ -import type { GlobScanOptions } from "bun"; -const { validateObject, validateString, validateFunction, validateArray } = require("internal/validators"); -const { sep } = require("node:path"); +// @ts-nocheck -- the bottom of this file embeds a vendored copy of minimatch +// (compiled JS, not written for this tsconfig); see lazyMinimatch(). +// +// Port of Node.js lib/internal/fs/glob.js (v26.3.0): +// https://github.com/nodejs/node/blob/50c35fea9e64d50ab3bb5f359e8523de89d6c798/lib/internal/fs/glob.js +// backed by a vendored copy of minimatch (Node's deps/minimatch/index.js, ISC license): +// https://github.com/nodejs/node/blob/50c35fea9e64d50ab3bb5f359e8523de89d6c798/deps/minimatch/index.js +// embedded verbatim below lazyMinimatch(); the vendored block is third-party +// and not edited — only the port above it follows bun's style. +// +// This is not backed by Bun.Glob: the vendored test-fs-glob.mjs (448 cases) +// exercises minimatch-specific semantics — withFileTypes Dirents, the +// exclude-array matching rules, symlink-walking and dotfile rules — that +// Bun.Glob.scan does not implement (328/448 fail with the Bun.Glob-backed +// version on main). Replace this with Bun.Glob once those gaps are closed +// natively. +const { validateObject, validateString, validateBoolean, validateArray } = require("internal/validators"); +const { join, resolve, basename, dirname, isAbsolute } = require("node:path"); +const { kEmptyObject } = require("internal/shared"); const isWindows = process.platform === "win32"; +const isMacOS = process.platform === "darwin"; -interface GlobOptions { - /** @default process.cwd() */ - cwd?: string; - exclude?: ((ent: string) => boolean) | string[]; - /** - * Should glob return paths as {@link Dirent} objects. `false` for strings. - * @default false */ - withFileTypes?: boolean; +// node:fs and node:fs/promises cannot be required at module scope: +// node:fs/promises requires this module at its top level. +let _fs; +function lazyFs() { + return (_fs ??= require("node:fs")); +} +let _fsPromises; +function lazyFsPromises() { + return (_fsPromises ??= require("node:fs/promises")); } -async function* glob(pattern: string | string[], options?: GlobOptions): AsyncGenerator { - const patterns = validatePattern(pattern); - const globOptions = mapOptions(options || {}); - const exclude = globOptions.exclude; - const excludeGlobs = Array.isArray(exclude) - ? exclude.flatMap(pattern => [new Bun.Glob(pattern), new Bun.Glob(pattern.replace(/\/+$/, "") + "/**")]) - : null; +function compareDirentName(a, b) { + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; +} +function identity(v) { + return v; +} +function nullOnReject() { + return null; +} +function emptyArrayOnReject() { + return []; +} +function excludeNothing(_path) { + return false; +} +function makeMatchersExclude(matchers) { + return function isExcludedByMatchers(value) { + for (const matcher of matchers) { + if (matcher.match(value)) return true; + } + return false; + }; +} +function statForFileTypes(cache, root, path) { + return cache.statSync(isAbsolute(path) ? path : join(root, path)); +} - for (const pat of patterns) { - for await (const ent of new Bun.Glob(pat).scan(globOptions)) { - if (typeof exclude === "function") { - if (exclude(ent)) continue; - } else if (excludeGlobs) { - if (excludeGlobs.some(glob => glob.match(ent))) { - continue; - } +const kStats = Symbol("stats"); +let _DirentFromStats: any; +function lazyDirentFromStats() { + if (_DirentFromStats === undefined) { + const { Dirent } = lazyFs(); + class DirentFromStats extends Dirent { + constructor(name, stats, path) { + super(name, null, path); + this[kStats] = stats; } - - yield ent; } + for (const key of [ + "isBlockDevice", + "isCharacterDevice", + "isDirectory", + "isFIFO", + "isFile", + "isSocket", + "isSymbolicLink", + ]) { + DirentFromStats.prototype[key] = function () { + return this[kStats][key](); + }; + } + _DirentFromStats = DirentFromStats; } + return _DirentFromStats; } -function* globSync(pattern: string | string[], options?: GlobOptions): Generator { - const patterns = validatePattern(pattern); - const globOptions = mapOptions(options || {}); - const exclude = globOptions.exclude; - const excludeGlobs = Array.isArray(exclude) - ? exclude.flatMap(pattern => [new Bun.Glob(pattern), new Bun.Glob(pattern.replace(/\/+$/, "") + "/**")]) - : null; +function toPathIfFileURL(url) { + if (url == null || typeof url === "string") { + return url; + } + if ( + url instanceof URL || + (typeof url === "object" && typeof url.href === "string" && typeof url.protocol === "string") + ) { + return Bun.fileURLToPath(url); + } + return url; +} - for (const pat of patterns) { - for (const ent of new Bun.Glob(pat).scanSync(globOptions)) { - if (typeof exclude === "function") { - if (exclude(ent)) continue; - } else if (excludeGlobs) { - if (excludeGlobs.some(glob => glob.match(ent))) { - continue; - } +async function getDirent(path) { + let stat; + try { + stat = await lazyFsPromises().lstat(path); + } catch { + return null; + } + const DirentFromStats = lazyDirentFromStats(); + return new DirentFromStats(basename(path), stat, dirname(path)); +} + +function sortDirents(dirents) { + return dirents.sort(compareDirentName); +} + +function getDirentSync(path) { + let stat; + try { + stat = lazyFs().lstatSync(path); + } catch { + return null; + } + const DirentFromStats = lazyDirentFromStats(); + return new DirentFromStats(basename(path), stat, dirname(path)); +} + +function validateStringArrayOrFunction(value, name) { + if ($isArray(value)) { + for (let i = 0; i < value.length; ++i) { + if (typeof value[i] !== "string") { + throw $ERR_INVALID_ARG_TYPE(`${name}[${i}]`, "string", value[i]); } + } + return; + } + if (typeof value !== "function") { + throw $ERR_INVALID_ARG_TYPE(name, ["string[]", "function"], value); + } +} + +function validateStringArray(value, name) { + validateArray(value, name); + for (let i = 0; i < value.length; ++i) { + validateString(value[i], `${name}[${i}]`); + } +} + +function createMatcher(pattern, options = kEmptyObject) { + const opts = { + __proto__: null, + nocase: isWindows || isMacOS, + windowsPathsNoEscape: true, + nonegate: true, + nocomment: true, + optimizationLevel: 2, + platform: process.platform, + nocaseMagicOnly: true, + ...options, + }; + return new (lazyMinimatch().Minimatch)(pattern, opts); +} - yield ent; +function cloneSet(values) { + const cloned = new Set(); + for (const value of values) { + cloned.add(value); + } + return cloned; +} + +class Cache { + #cache = new Map(); + #statsCache = new Map(); + #followStatsCache = new Map(); + #readdirCache = new Map(); + #realpathCache = new Map(); + + stat(path) { + const cached = this.#statsCache.get(path); + if (cached) { + return cached; + } + const promise = getDirent(path); + this.#statsCache.set(path, promise); + return promise; + } + statSync(path) { + const cached = this.#statsCache.get(path); + // Do not return a promise from a sync function. + if (cached && !(cached instanceof Promise)) { + return cached; + } + const val = getDirentSync(path); + this.#statsCache.set(path, val); + return val; + } + followStat(path) { + const cached = this.#followStatsCache.get(path); + if (cached) { + return cached; + } + const promise = lazyFsPromises().stat(path).then(identity, nullOnReject); + this.#followStatsCache.set(path, promise); + return promise; + } + followStatSync(path) { + const cached = this.#followStatsCache.get(path); + if (cached && !(cached instanceof Promise)) { + return cached; + } + let val; + try { + val = lazyFs().statSync(path); + } catch { + val = null; + } + this.#followStatsCache.set(path, val); + return val; + } + realpath(path) { + const cached = this.#realpathCache.get(path); + if (cached) { + return cached; + } + const promise = lazyFsPromises().realpath(path).then(identity, nullOnReject); + this.#realpathCache.set(path, promise); + return promise; + } + realpathSync(path) { + const cached = this.#realpathCache.get(path); + if (cached && !(cached instanceof Promise)) { + return cached; + } + let val; + try { + val = lazyFs().realpathSync(path); + } catch { + val = null; + } + this.#realpathCache.set(path, val); + return val; + } + addToStatCache(path, val) { + this.#statsCache.set(path, val); + } + async readdir(path) { + const cached = this.#readdirCache.get(path); + if (cached) { + return cached; + } + const promise = lazyFsPromises().readdir(path, { __proto__: null, withFileTypes: true }).then( + // The traversal bookkeeping (the seen-cache and the "**/.." queueing) + // is sensitive to the order directory entries are visited in; some + // orders make it drop results (reproducible in node itself by feeding + // it the same order). Sort entries so traversal is deterministic and + // matches the orders the upstream algorithm is known to handle. + sortDirents, + emptyArrayOnReject, + ); + this.#readdirCache.set(path, promise); + return promise; + } + readdirSync(path) { + const cached = this.#readdirCache.get(path); + if (cached) { + return cached; + } + let val; + try { + // Sorted for deterministic traversal; see the comment in readdir(). + val = sortDirents(lazyFs().readdirSync(path, { __proto__: null, withFileTypes: true })); + } catch { + val = []; + } + this.#readdirCache.set(path, val); + return val; + } + add(path, pattern) { + let cache = this.#cache.get(path); + if (!cache) { + cache = new Set(); + this.#cache.set(path, cache); } + const originalSize = cache.size; + for (const index of pattern.indexes) { + cache.add(pattern.cacheKey(index)); + } + return cache.size !== originalSize + pattern.indexes.size; + } + seen(path, pattern, index) { + return this.#cache.get(path)?.has(pattern.cacheKey(index)); } } -function validatePattern(pattern: string | string[]): string[] { - if (Array.isArray(pattern)) { - validateArray(pattern, "pattern"); - return pattern.map(p => { - validateString(p, "pattern"); - return isWindows ? p.replaceAll("/", sep) : p; - }); +class Pattern { + #pattern; + #globStrings; + indexes; + symlinks; + realpaths; + last; + + constructor(pattern, globStrings, indexes, symlinks, realpaths = new Set()) { + this.#pattern = pattern; + this.#globStrings = globStrings; + this.indexes = indexes; + this.symlinks = symlinks; + this.realpaths = realpaths; + this.last = pattern.length - 1; } - validateString(pattern, "pattern"); - return [isWindows ? pattern.replaceAll("/", sep) : pattern]; + isLast(isDirectory) { + return ( + this.indexes.has(this.last) || + (this.at(-1) === "" && isDirectory && this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR) + ); + } + isFirst() { + return this.indexes.has(0); + } + get hasSeenSymlinks() { + for (const i of this.indexes) { + if (!this.symlinks.has(i)) return true; + } + return false; + } + at(index) { + return this.#pattern.at(index); + } + child(indexes, symlinks = new Set(), realpaths = this.realpaths) { + return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks, realpaths); + } + test(index, path) { + if (index > this.#pattern.length) { + return false; + } + const pattern = this.#pattern[index]; + if (pattern === lazyMinimatch().GLOBSTAR) { + return true; + } + if (typeof pattern === "string") { + return pattern === path; + } + if (typeof pattern?.test === "function") { + return pattern.test(path); + } + return false; + } + + cacheKey(index) { + let key = ""; + for (let i = index; i < this.#globStrings.length; i++) { + key += this.#globStrings[i]; + if (i !== this.#globStrings.length - 1) { + key += "/"; + } + } + return key; + } } -function mapOptions(options: GlobOptions): GlobScanOptions & { exclude: GlobOptions["exclude"] } { - validateObject(options, "options"); +class ResultSet extends Set { + #root = "."; + #isExcluded = excludeNothing; + + setup(root, isExcludedFn) { + this.#root = root; + this.#isExcluded = isExcludedFn; + } - let exclude = options.exclude ?? no; - if (Array.isArray(exclude)) { - validateArray(exclude, "options.exclude"); - if (isWindows) { - exclude = exclude.map((pattern: string) => pattern.replaceAll("\\", "/")); + add(value): any { + if (this.#isExcluded(resolve(this.#root, value))) { + return false; } - } else { - validateFunction(exclude, "options.exclude"); + super.add(value); + return true; } +} - if (options.withFileTypes) { - throw new TypeError("fs.glob does not support options.withFileTypes yet. Please open an issue on GitHub."); +class Glob { + #root; + #exclude; + #cache = new Cache(); + #results = new ResultSet(); + #queue: Array<{ path: string; patterns: Pattern[] }> = []; + #subpatterns = new Map(); + #patterns; + #withFileTypes; + #followSymlinks = false; + #isExcluded = excludeNothing; + matchers; + constructor(pattern, options = kEmptyObject) { + validateObject(options, "options"); + const { exclude, cwd, followSymlinks, withFileTypes } = options; + this.#root = toPathIfFileURL(cwd) ?? process.cwd(); + if (followSymlinks != null) { + validateBoolean(followSymlinks, "options.followSymlinks"); + this.#followSymlinks = followSymlinks; + } + this.#withFileTypes = !!withFileTypes; + if (exclude != null) { + validateStringArrayOrFunction(exclude, "options.exclude"); + if ($isArray(exclude)) { + // Convert the path part of exclude patterns to absolute paths for + // consistent comparison before instantiating matchers. + const matchers = []; + for (const pat of exclude) { + matchers.push(createMatcher(resolve(this.#root, pat))); + } + this.#isExcluded = makeMatchersExclude(matchers); + this.#results.setup(this.#root, this.#isExcluded); + } else { + this.#exclude = exclude; + } + } + let patterns; + if (typeof pattern === "object") { + validateStringArray(pattern, "patterns"); + patterns = pattern; + } else { + validateString(pattern, "patterns"); + patterns = [pattern]; + } + this.matchers = []; + this.#patterns = []; + for (const pat of patterns) { + const matcher = createMatcher(pat); + this.matchers.push(matcher); + for (let i = 0; i < matcher.set.length; i++) { + this.#patterns.push(new Pattern(matcher.set[i], matcher.globParts[i], new Set().add(0), new Set())); + } + } } - return { - // NOTE: this is subtly different from Glob's default behavior. - // `process.cwd()` may be overridden by JS code, but native code will used the - // cached `getcwd` on BunProcess. - cwd: options?.cwd ?? process.cwd(), - // https://github.com/nodejs/node/blob/a9546024975d0bfb0a8ae47da323b10fb5cbb88b/lib/internal/fs/glob.js#L655 - followSymlinks: true, - // https://github.com/oven-sh/bun/issues/20507 - onlyFiles: false, - exclude, - }; + globSync() { + this.#queue.push({ __proto__: null, path: ".", patterns: this.#patterns }); + while (this.#queue.length > 0) { + const item = this.#queue.pop()!; + for (let i = 0; i < item.patterns.length; i++) { + this.#addSubpatterns(item.path, item.patterns[i]); + } + for (const [path, patterns] of this.#subpatterns) { + this.#queue.push({ __proto__: null, path, patterns }); + } + this.#subpatterns.clear(); + } + return Array.from( + this.#results, + this.#withFileTypes ? statForFileTypes.bind(null, this.#cache, this.#root) : undefined, + ); + } + #isDirectorySync(path, stat, pattern) { + if (stat?.isDirectory()) { + return true; + } + if (!stat?.isSymbolicLink()) { + return false; + } + if (this.#followSymlinks) { + return !!this.#cache.followStatSync(path)?.isDirectory(); + } + return pattern.hasSeenSymlinks; + } + async #isDirectory(path, stat, pattern) { + if (stat?.isDirectory()) { + return true; + } + if (!stat?.isSymbolicLink()) { + return false; + } + if (this.#followSymlinks) { + return !!(await this.#cache.followStat(path))?.isDirectory(); + } + return pattern.hasSeenSymlinks; + } + #nextRealpathsSync(path, isDirectory, pattern) { + if (!this.#followSymlinks || !isDirectory) { + return pattern.realpaths; + } + const real = this.#cache.realpathSync(path); + if (real === null) { + return pattern.realpaths; + } + const realpaths = cloneSet(pattern.realpaths); + realpaths.add(real); + return realpaths; + } + async #nextRealpaths(path, isDirectory, pattern) { + if (!this.#followSymlinks || !isDirectory) { + return pattern.realpaths; + } + const real = await this.#cache.realpath(path); + if (real === null) { + return pattern.realpaths; + } + const realpaths = cloneSet(pattern.realpaths); + realpaths.add(real); + return realpaths; + } + async #isCyclic(path, isDirectory, pattern) { + if (!this.#followSymlinks || !isDirectory) { + return false; + } + const real = await this.#cache.realpath(path); + return real !== null && pattern.realpaths.has(real); + } + #isCyclicSync(path, isDirectory, pattern) { + if (!this.#followSymlinks || !isDirectory) { + return false; + } + const real = this.#cache.realpathSync(path); + return real !== null && pattern.realpaths.has(real); + } + #addSubpattern(path, pattern) { + if (this.#isExcluded(path)) { + return; + } + const fullpath = resolve(this.#root, path); + + // If path is a directory, add trailing slash and test patterns again. + if (this.#isExcluded(`${fullpath}/`) && this.#cache.statSync(fullpath).isDirectory()) { + return; + } + + if (this.#exclude) { + if (this.#withFileTypes) { + // Key by absolute path: the stat cache is populated with entry + // fullpaths, and a relative lstat would resolve against + // process.cwd() instead of options.cwd (upstream passes `path` + // here, which silently skips the exclude callback when cwd + // differs). + const stat = this.#cache.statSync(fullpath); + if (stat !== null) { + if (this.#exclude(stat)) { + return; + } + } + } else if (this.#exclude(path)) { + return; + } + } + if (!this.#subpatterns.has(path)) { + this.#subpatterns.set(path, [pattern]); + } else { + this.#subpatterns.get(path).push(pattern); + } + } + #addSubpatterns(path, pattern) { + const seen = this.#cache.add(path, pattern); + if (seen) { + return; + } + const fullpath = resolve(this.#root, path); + const stat = this.#cache.statSync(fullpath); + const last = pattern.last; + const isDirectory = this.#isDirectorySync(fullpath, stat, pattern); + const isLast = pattern.isLast(isDirectory); + const isFirst = pattern.isFirst(); + + if (this.#isExcluded(fullpath)) { + return; + } + if (isFirst && isWindows && typeof pattern.at(0) === "string" && pattern.at(0).endsWith(":")) { + // Absolute path, go to root + this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === "") { + // Absolute path, go to root + this.#addSubpattern("/", pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === "..") { + // Start with .., go to parent + this.#addSubpattern("../", pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === ".") { + // Start with ., proceed + this.#addSubpattern(".", pattern.child(new Set().add(1))); + return; + } + + if (isLast && typeof pattern.at(-1) === "string") { + // Add result if it exists + const p = pattern.at(-1); + const stat = this.#cache.statSync(join(fullpath, p)); + if (stat && (p || isDirectory)) { + this.#results.add(join(path, p)); + } + if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { + return; + } + } else if ( + isLast && + pattern.at(-1) === lazyMinimatch().GLOBSTAR && + (path !== "." || pattern.at(0) === "." || (last === 0 && stat)) + ) { + // If pattern ends with **, add to results + // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" + this.#results.add(path); + } + + if (!isDirectory || this.#isCyclicSync(fullpath, isDirectory, pattern)) { + return; + } + + const nextRealpaths = this.#nextRealpathsSync(fullpath, isDirectory, pattern); + + let children; + const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); + if (typeof firstPattern === "string") { + const stat = this.#cache.statSync(join(fullpath, firstPattern)); + if (stat) { + setDirentName(stat, firstPattern); + children = [stat]; + } else { + return; + } + } else { + children = this.#cache.readdirSync(fullpath); + } + + for (let i = 0; i < children.length; i++) { + const entry = children[i]; + const entryPath = join(path, entry.name); + const entryFullpath = join(fullpath, entry.name); + this.#cache.addToStatCache(entryFullpath, entry); + const entryIsDirectory = + entry.isDirectory() || + (this.#followSymlinks && entry.isSymbolicLink() && !!this.#cache.followStatSync(entryFullpath)?.isDirectory()); + + const subPatterns = new Set(); + const nSymlinks = new Set(); + for (const index of pattern.indexes) { + // For each child, check potential patterns + if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { + return; + } + const current = pattern.at(index); + const nextIndex = index + 1; + const next = pattern.at(nextIndex); + const fromSymlink = !this.#followSymlinks && pattern.symlinks.has(index); + + if (current === lazyMinimatch().GLOBSTAR) { + const isDot = entry.name[0] === "."; + const nextMatches = pattern.test(nextIndex, entry.name); + + let nextNonGlobIndex = nextIndex; + while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) { + nextNonGlobIndex++; + } + + const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name); + + if ((isDot && !matchesDot) || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + continue; + } + if (!fromSymlink && entryIsDirectory) { + // If directory, add ** to its potential patterns + subPatterns.add(index); + } else if (!fromSymlink && index === last) { + // If ** is last, add to results + this.#results.add(entryPath); + } + + // Any pattern after ** is also a potential pattern + // so we can already test it here + if (nextMatches && nextIndex === last && !isLast) { + // If next pattern is the last one, add to results + this.#results.add(entryPath); + } else if (nextMatches && entryIsDirectory) { + // Pattern matched, meaning two patterns forward + // are also potential patterns + // e.g **/b/c when entry is a/b - add c to potential patterns + subPatterns.add(index + 2); + } + if ((nextMatches || pattern.at(0) === ".") && (entryIsDirectory || entry.isSymbolicLink()) && !fromSymlink) { + // If pattern after ** matches, or pattern starts with "." + // and entry is a directory or symlink, add to potential patterns + subPatterns.add(nextIndex); + } + + if (!this.#followSymlinks && entry.isSymbolicLink()) { + nSymlinks.add(index); + } + + if (next === ".." && entryIsDirectory) { + // In case pattern is "**/..", + // both parent and current directory should be added to the queue + // if this is the last pattern, add to results instead + const parent = join(path, ".."); + if (nextIndex < last) { + if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { + this.#subpatterns.set(path, [pattern.child(new Set().add(nextIndex + 1))]); + } + if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { + this.#subpatterns.set(parent, [pattern.child(new Set().add(nextIndex + 1))]); + } + } else { + if (!this.#cache.seen(path, pattern, nextIndex)) { + this.#cache.add(path, pattern.child(new Set().add(nextIndex))); + this.#results.add(path); + } + if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { + this.#cache.add(parent, pattern.child(new Set().add(nextIndex))); + this.#results.add(parent); + } + } + } + } + if (typeof current === "string") { + if (pattern.test(index, entry.name) && index !== last) { + // If current pattern matches entry name + // the next pattern is a potential pattern + subPatterns.add(nextIndex); + } else if (current === "." && pattern.test(nextIndex, entry.name)) { + // If current pattern is ".", proceed to test next pattern + if (nextIndex === last) { + this.#results.add(entryPath); + } else { + subPatterns.add(nextIndex + 1); + } + } + } + if (typeof current === "object" && pattern.test(index, entry.name)) { + // If current pattern is a regex that matches entry name (e.g *.js) + // add next pattern to potential patterns, or to results if it's the last pattern + if (index === last) { + this.#results.add(entryPath); + } else if (entryIsDirectory) { + subPatterns.add(nextIndex); + } + } + } + if (subPatterns.size > 0) { + // If there are potential patterns, add to queue + this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks, nextRealpaths)); + } + } + } + + async *glob() { + this.#queue.push({ __proto__: null, path: ".", patterns: this.#patterns }); + while (this.#queue.length > 0) { + const item = this.#queue.pop()!; + for (let i = 0; i < item.patterns.length; i++) { + yield* this.#iterateSubpatterns(item.path, item.patterns[i]); + } + for (const [path, patterns] of this.#subpatterns) { + this.#queue.push({ __proto__: null, path, patterns }); + } + this.#subpatterns.clear(); + } + } + async *#iterateSubpatterns(path, pattern) { + const seen = this.#cache.add(path, pattern); + if (seen) { + return; + } + const fullpath = resolve(this.#root, path); + const stat = await this.#cache.stat(fullpath); + const last = pattern.last; + const isDirectory = await this.#isDirectory(fullpath, stat, pattern); + const isLast = pattern.isLast(isDirectory); + const isFirst = pattern.isFirst(); + + if (this.#isExcluded(fullpath)) { + return; + } + if (isFirst && isWindows && typeof pattern.at(0) === "string" && pattern.at(0).endsWith(":")) { + // Absolute path, go to root + this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === "") { + // Absolute path, go to root + this.#addSubpattern("/", pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === "..") { + // Start with .., go to parent + this.#addSubpattern("../", pattern.child(new Set().add(1))); + return; + } + if (isFirst && pattern.at(0) === ".") { + // Start with ., proceed + this.#addSubpattern(".", pattern.child(new Set().add(1))); + return; + } + + if (isLast && typeof pattern.at(-1) === "string") { + // Add result if it exists + const p = pattern.at(-1); + const stat = await this.#cache.stat(join(fullpath, p)); + if (stat && (p || isDirectory)) { + const result = join(path, p); + if (!this.#results.has(result)) { + if (this.#results.add(result)) { + yield this.#withFileTypes ? stat : result; + } + } + } + if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { + return; + } + } else if ( + isLast && + pattern.at(-1) === lazyMinimatch().GLOBSTAR && + (path !== "." || pattern.at(0) === "." || (last === 0 && stat)) + ) { + // If pattern ends with **, add to results + // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" + if (!this.#results.has(path)) { + if (this.#results.add(path)) { + yield this.#withFileTypes ? stat : path; + } + } + } + + if (!isDirectory || (await this.#isCyclic(fullpath, isDirectory, pattern))) { + return; + } + + const nextRealpaths = await this.#nextRealpaths(fullpath, isDirectory, pattern); + + let children; + const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); + if (typeof firstPattern === "string") { + const stat = await this.#cache.stat(join(fullpath, firstPattern)); + if (stat) { + setDirentName(stat, firstPattern); + children = [stat]; + } else { + return; + } + } else { + children = await this.#cache.readdir(fullpath); + } + + for (let i = 0; i < children.length; i++) { + const entry = children[i]; + const entryPath = join(path, entry.name); + const entryFullpath = join(fullpath, entry.name); + this.#cache.addToStatCache(entryFullpath, entry); + const entryIsDirectory = + entry.isDirectory() || + (this.#followSymlinks && + entry.isSymbolicLink() && + !!(await this.#cache.followStat(entryFullpath))?.isDirectory()); + + const subPatterns = new Set(); + const nSymlinks = new Set(); + for (const index of pattern.indexes) { + // For each child, check potential patterns + if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { + return; + } + const current = pattern.at(index); + const nextIndex = index + 1; + const next = pattern.at(nextIndex); + const fromSymlink = !this.#followSymlinks && pattern.symlinks.has(index); + + if (current === lazyMinimatch().GLOBSTAR) { + const isDot = entry.name[0] === "."; + const nextMatches = pattern.test(nextIndex, entry.name); + + let nextNonGlobIndex = nextIndex; + while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) { + nextNonGlobIndex++; + } + + const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name); + + if ((isDot && !matchesDot) || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + continue; + } + if (!fromSymlink && entryIsDirectory) { + // If directory, add ** to its potential patterns + subPatterns.add(index); + } else if (!fromSymlink && index === last) { + // If ** is last, add to results + if (!this.#results.has(entryPath) && this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; + } + } + + // Any pattern after ** is also a potential pattern + // so we can already test it here + if (nextMatches && nextIndex === last && !isLast) { + // If next pattern is the last one, add to results + if (!this.#results.has(entryPath) && this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; + } + } else if (nextMatches && entryIsDirectory) { + // Pattern matched, meaning two patterns forward + // are also potential patterns + // e.g **/b/c when entry is a/b - add c to potential patterns + subPatterns.add(index + 2); + } + if ((nextMatches || pattern.at(0) === ".") && (entryIsDirectory || entry.isSymbolicLink()) && !fromSymlink) { + // If pattern after ** matches, or pattern starts with "." + // and entry is a directory or symlink, add to potential patterns + subPatterns.add(nextIndex); + } + + if (!this.#followSymlinks && entry.isSymbolicLink()) { + nSymlinks.add(index); + } + + if (next === ".." && entryIsDirectory) { + // In case pattern is "**/..", + // both parent and current directory should be added to the queue + // if this is the last pattern, add to results instead + const parent = join(path, ".."); + if (nextIndex < last) { + if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { + this.#subpatterns.set(path, [pattern.child(new Set().add(nextIndex + 1))]); + } + if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { + this.#subpatterns.set(parent, [pattern.child(new Set().add(nextIndex + 1))]); + } + } else { + if (!this.#cache.seen(path, pattern, nextIndex)) { + this.#cache.add(path, pattern.child(new Set().add(nextIndex))); + if (!this.#results.has(path)) { + if (this.#results.add(path)) { + yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path; + } + } + } + if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { + this.#cache.add(parent, pattern.child(new Set().add(nextIndex))); + if (!this.#results.has(parent)) { + if (this.#results.add(parent)) { + yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent; + } + } + } + } + } + } + if (typeof current === "string") { + if (pattern.test(index, entry.name) && index !== last) { + // If current pattern matches entry name + // the next pattern is a potential pattern + subPatterns.add(nextIndex); + } else if (current === "." && pattern.test(nextIndex, entry.name)) { + // If current pattern is ".", proceed to test next pattern + if (nextIndex === last) { + if (!this.#results.has(entryPath)) { + if (this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; + } + } + } else { + subPatterns.add(nextIndex + 1); + } + } + } + if (typeof current === "object" && pattern.test(index, entry.name)) { + // If current pattern is a regex that matches entry name (e.g *.js) + // add next pattern to potential patterns, or to results if it's the last pattern + if (index === last) { + if (!this.#results.has(entryPath)) { + if (this.#results.add(entryPath)) { + yield this.#withFileTypes ? entry : entryPath; + } + } + } else if (entryIsDirectory) { + subPatterns.add(nextIndex); + } + } + } + if (subPatterns.size > 0) { + // If there are potential patterns, add to queue + this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks, nextRealpaths)); + } + } + } +} + +// `name` may not be writable on native Dirent instances. +function setDirentName(dirent, name) { + try { + dirent.name = name; + } catch { + Object.defineProperty(dirent, "name", { + value: name, + writable: true, + enumerable: true, + configurable: true, + }); + } +} + +function glob(pattern, options) { + return new Glob(pattern, options).glob(); } -// `var` avoids TDZ checks. -var no = _ => false; +function globSync(pattern, options) { + return new Glob(pattern, options).globSync(); +} + +export default { glob, globSync, Glob }; + +let _minimatch: any; +// ───────────────────────────────────────────────────────────────────────── +// Vendored: Node.js deps/minimatch/index.js (v26.3.0, ISC license), unmodified. +// https://github.com/nodejs/node/blob/50c35fea9e64d50ab3bb5f359e8523de89d6c798/deps/minimatch/index.js +// ───────────────────────────────────────────────────────────────────────── +function lazyMinimatch() { + if (_minimatch) return _minimatch; + const exports: any = {}; + const module = { exports }; + // --- begin vendored minimatch (Node v26.3.0 deps/minimatch/index.js) --- + var __getOwnPropNames = Object.getOwnPropertyNames; + var __commonJS = (cb, mod) => + function __require() { + return (mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports); + }; + + // node_modules/balanced-match/dist/commonjs/index.js + var require_commonjs = __commonJS({ + "node_modules/balanced-match/dist/commonjs/index.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.range = exports2.balanced = void 0; + var balanced = (a, b, str) => { + const ma = a instanceof RegExp ? maybeMatch(a, str) : a; + const mb = b instanceof RegExp ? maybeMatch(b, str) : b; + const r = ma !== null && mb != null && (0, exports2.range)(ma, mb, str); + return ( + r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + ma.length, r[1]), + post: str.slice(r[1] + mb.length), + } + ); + }; + exports2.balanced = balanced; + var maybeMatch = (reg, str) => { + const m = str.match(reg); + return m ? m[0] : null; + }; + var range = (a, b, str) => { + let begs, + beg, + left, + right = void 0, + result; + let ai = str.indexOf(a); + let bi = str.indexOf(b, ai + 1); + let i = ai; + if (ai >= 0 && bi > 0) { + if (a === b) { + return [ai, bi]; + } + begs = []; + left = str.length; + while (i >= 0 && !result) { + if (i === ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length === 1) { + const r = begs.pop(); + if (r !== void 0) result = [r, bi]; + } else { + beg = begs.pop(); + if (beg !== void 0 && beg < left) { + left = beg; + right = bi; + } + bi = str.indexOf(b, i + 1); + } + i = ai < bi && ai >= 0 ? ai : bi; + } + if (begs.length && right !== void 0) { + result = [left, right]; + } + } + return result; + }; + exports2.range = range; + }, + }); + + // node_modules/brace-expansion/dist/commonjs/index.js + var require_commonjs2 = __commonJS({ + "node_modules/brace-expansion/dist/commonjs/index.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.EXPANSION_MAX = void 0; + exports2.expand = expand; + var balanced_match_1 = require_commonjs(); + var escSlash = "\0SLASH" + Math.random() + "\0"; + var escOpen = "\0OPEN" + Math.random() + "\0"; + var escClose = "\0CLOSE" + Math.random() + "\0"; + var escComma = "\0COMMA" + Math.random() + "\0"; + var escPeriod = "\0PERIOD" + Math.random() + "\0"; + var escSlashPattern = new RegExp(escSlash, "g"); + var escOpenPattern = new RegExp(escOpen, "g"); + var escClosePattern = new RegExp(escClose, "g"); + var escCommaPattern = new RegExp(escComma, "g"); + var escPeriodPattern = new RegExp(escPeriod, "g"); + var slashPattern = /\\\\/g; + var openPattern = /\\{/g; + var closePattern = /\\}/g; + var commaPattern = /\\,/g; + var periodPattern = /\\\./g; + exports2.EXPANSION_MAX = 1e5; + function numeric(str) { + return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); + } + function escapeBraces(str) { + return str + .replace(slashPattern, escSlash) + .replace(openPattern, escOpen) + .replace(closePattern, escClose) + .replace(commaPattern, escComma) + .replace(periodPattern, escPeriod); + } + function unescapeBraces(str) { + return str + .replace(escSlashPattern, "\\") + .replace(escOpenPattern, "{") + .replace(escClosePattern, "}") + .replace(escCommaPattern, ",") + .replace(escPeriodPattern, "."); + } + function parseCommaParts(str) { + if (!str) { + return [""]; + } + const parts = []; + const m = (0, balanced_match_1.balanced)("{", "}", str); + if (!m) { + return str.split(","); + } + const { pre, body, post } = m; + const p = pre.split(","); + p[p.length - 1] += "{" + body + "}"; + const postParts = parseCommaParts(post); + if (post.length) { + p[p.length - 1] += postParts.shift(); + p.push(...postParts); + } + parts.push(...p); + return parts; + } + function expand(str, options = {}) { + if (!str) { + return []; + } + const { max = exports2.EXPANSION_MAX } = options; + if (str.slice(0, 2) === "{}") { + str = "\\{\\}" + str.slice(2); + } + return expand_(escapeBraces(str), max, true).map(unescapeBraces); + } + function embrace(str) { + return "{" + str + "}"; + } + function isPadded(el) { + return /^-?0\d/.test(el); + } + function lte(i, y) { + return i <= y; + } + function gte(i, y) { + return i >= y; + } + function expand_(str, max, isTop) { + const expansions = []; + const m = (0, balanced_match_1.balanced)("{", "}", str); + if (!m) return [str]; + const pre = m.pre; + const post = m.post.length ? expand_(m.post, max, false) : [""]; + if (m.pre.endsWith("$")) { + for (let k = 0; k < post.length && k < max; k++) { + const expansion = pre + "{" + m.body + "}" + post[k]; + expansions.push(expansion); + } + } else { + const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + const isSequence = isNumericSequence || isAlphaSequence; + const isOptions = m.body.indexOf(",") >= 0; + if (!isSequence && !isOptions) { + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + "{" + m.body + escClose + m.post; + return expand_(str, max, true); + } + return [str]; + } + let n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1 && n[0] !== void 0) { + n = expand_(n[0], max, false).map(embrace); + if (n.length === 1) { + return post.map(p => m.pre + n[0] + p); + } + } + } + let N; + if (isSequence && n[0] !== void 0 && n[1] !== void 0) { + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length); + let incr = n.length === 3 && n[2] !== void 0 ? Math.max(Math.abs(numeric(n[2])), 1) : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + const pad = n.some(isPadded); + N = []; + for (let i = x; test(i, y); i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === "\\") { + c = ""; + } + } else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join("0"); + if (i < 0) { + c = "-" + z + c.slice(1); + } else { + c = z + c; + } + } + } + } + N.push(c); + } + } else { + N = []; + for (let j = 0; j < n.length; j++) { + N.push(...expand_(n[j], max, false)); + } + } + for (let j = 0; j < N.length; j++) { + for (let k = 0; k < post.length && expansions.length < max; k++) { + const expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) { + expansions.push(expansion); + } + } + } + } + return expansions; + } + }, + }); + + // dist/commonjs/assert-valid-pattern.js + var require_assert_valid_pattern = __commonJS({ + "dist/commonjs/assert-valid-pattern.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.assertValidPattern = void 0; + var MAX_PATTERN_LENGTH = 1024 * 64; + var assertValidPattern = pattern => { + if (typeof pattern !== "string") { + throw new TypeError("invalid pattern"); + } + if (pattern.length > MAX_PATTERN_LENGTH) { + throw new TypeError("pattern is too long"); + } + }; + exports2.assertValidPattern = assertValidPattern; + }, + }); + + // dist/commonjs/brace-expressions.js + var require_brace_expressions = __commonJS({ + "dist/commonjs/brace-expressions.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.parseClass = void 0; + var posixClasses = { + "[:alnum:]": ["\\p{L}\\p{Nl}\\p{Nd}", true], + "[:alpha:]": ["\\p{L}\\p{Nl}", true], + "[:ascii:]": ["\\x00-\\x7f", false], + "[:blank:]": ["\\p{Zs}\\t", true], + "[:cntrl:]": ["\\p{Cc}", true], + "[:digit:]": ["\\p{Nd}", true], + "[:graph:]": ["\\p{Z}\\p{C}", true, true], + "[:lower:]": ["\\p{Ll}", true], + "[:print:]": ["\\p{C}", true], + "[:punct:]": ["\\p{P}", true], + "[:space:]": ["\\p{Z}\\t\\r\\n\\v\\f", true], + "[:upper:]": ["\\p{Lu}", true], + "[:word:]": ["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true], + "[:xdigit:]": ["A-Fa-f0-9", false], + }; + var braceEscape = s => s.replace(/[[\]\\-]/g, "\\$&"); + var regexpEscape = s => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + var rangesToString = ranges => ranges.join(""); + var parseClass = (glob, position) => { + const pos = position; + if (glob.charAt(pos) !== "[") { + throw new Error("not in a brace expression"); + } + const ranges = []; + const negs = []; + let i = pos + 1; + let sawStart = false; + let uflag = false; + let escaping = false; + let negate = false; + let endPos = pos; + let rangeStart = ""; + WHILE: while (i < glob.length) { + const c = glob.charAt(i); + if ((c === "!" || c === "^") && i === pos + 1) { + negate = true; + i++; + continue; + } + if (c === "]" && sawStart && !escaping) { + endPos = i + 1; + break; + } + sawStart = true; + if (c === "\\") { + if (!escaping) { + escaping = true; + i++; + continue; + } + } + if (c === "[" && !escaping) { + for (const [cls, [unip, u, neg]] of Object.entries(posixClasses)) { + if (glob.startsWith(cls, i)) { + if (rangeStart) { + return ["$.", false, glob.length - pos, true]; + } + i += cls.length; + if (neg) negs.push(unip); + else ranges.push(unip); + uflag = uflag || u; + continue WHILE; + } + } + } + escaping = false; + if (rangeStart) { + if (c > rangeStart) { + ranges.push(braceEscape(rangeStart) + "-" + braceEscape(c)); + } else if (c === rangeStart) { + ranges.push(braceEscape(c)); + } + rangeStart = ""; + i++; + continue; + } + if (glob.startsWith("-]", i + 1)) { + ranges.push(braceEscape(c + "-")); + i += 2; + continue; + } + if (glob.startsWith("-", i + 1)) { + rangeStart = c; + i += 2; + continue; + } + ranges.push(braceEscape(c)); + i++; + } + if (endPos < i) { + return ["", false, 0, false]; + } + if (!ranges.length && !negs.length) { + return ["$.", false, glob.length - pos, true]; + } + if (negs.length === 0 && ranges.length === 1 && /^\\?.$/.test(ranges[0]) && !negate) { + const r = ranges[0].length === 2 ? ranges[0].slice(-1) : ranges[0]; + return [regexpEscape(r), false, endPos - pos, false]; + } + const sranges = "[" + (negate ? "^" : "") + rangesToString(ranges) + "]"; + const snegs = "[" + (negate ? "" : "^") + rangesToString(negs) + "]"; + const comb = ranges.length && negs.length ? "(" + sranges + "|" + snegs + ")" : ranges.length ? sranges : snegs; + return [comb, uflag, endPos - pos, true]; + }; + exports2.parseClass = parseClass; + }, + }); + + // dist/commonjs/unescape.js + var require_unescape = __commonJS({ + "dist/commonjs/unescape.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.unescape = void 0; + var unescape = (s, { windowsPathsNoEscape = false, magicalBraces = true } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape + ? s.replace(/\[([^/\\])\]/g, "$1") + : s.replace(/((?!\\).|^)\[([^/\\])\]/g, "$1$2").replace(/\\([^/])/g, "$1"); + } + return windowsPathsNoEscape + ? s.replace(/\[([^/\\{}])\]/g, "$1") + : s.replace(/((?!\\).|^)\[([^/\\{}])\]/g, "$1$2").replace(/\\([^/{}])/g, "$1"); + }; + exports2.unescape = unescape; + }, + }); + + // dist/commonjs/ast.js + var require_ast = __commonJS({ + "dist/commonjs/ast.js"(exports2) { + "use strict"; + var _a; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.AST = void 0; + var brace_expressions_js_1 = require_brace_expressions(); + var unescape_js_12 = require_unescape(); + var types = /* @__PURE__ */ new Set(["!", "?", "+", "*", "@"]); + var isExtglobType = c => types.has(c); + var isExtglobAST = c => isExtglobType(c.type); + var adoptionMap = /* @__PURE__ */ new Map([ + ["!", ["@"]], + ["?", ["?", "@"]], + ["@", ["@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@"]], + ]); + var adoptionWithSpaceMap = /* @__PURE__ */ new Map([ + ["!", ["?"]], + ["@", ["?"]], + ["+", ["?", "*"]], + ]); + var adoptionAnyMap = /* @__PURE__ */ new Map([ + ["!", ["?", "@"]], + ["?", ["?", "@"]], + ["@", ["?", "@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@", "?", "*"]], + ]); + var usurpMap = /* @__PURE__ */ new Map([ + ["!", /* @__PURE__ */ new Map([["!", "@"]])], + [ + "?", + /* @__PURE__ */ new Map([ + ["*", "*"], + ["+", "*"], + ]), + ], + [ + "@", + /* @__PURE__ */ new Map([ + ["!", "!"], + ["?", "?"], + ["@", "@"], + ["*", "*"], + ["+", "+"], + ]), + ], + [ + "+", + /* @__PURE__ */ new Map([ + ["?", "*"], + ["*", "*"], + ]), + ], + ]); + var startNoTraversal = "(?!(?:^|/)\\.\\.?(?:$|/))"; + var startNoDot = "(?!\\.)"; + var addPatternStart = /* @__PURE__ */ new Set(["[", "."]); + var justDots = /* @__PURE__ */ new Set(["..", "."]); + var reSpecials = new Set("().*{}+?[]^$\\!"); + var regExpEscape2 = s => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + var qmark2 = "[^/]"; + var star2 = qmark2 + "*?"; + var starNoEmpty = qmark2 + "+?"; + var ID = 0; + var AST = class { + type; + #root; + #hasMagic; + #uflag = false; + #parts = []; + #parent; + #parentIndex; + #negs; + #filledNegs = false; + #options; + #toString; + // set to true if it's an extglob with no children + // (which really means one child of '') + #emptyExt = false; + id = ++ID; + get depth() { + return (this.#parent?.depth ?? -1) + 1; + } + [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() { + return { + "@@type": "AST", + id: this.id, + type: this.type, + root: this.#root.id, + parent: this.#parent?.id, + depth: this.depth, + partsLength: this.#parts.length, + parts: this.#parts, + }; + } + constructor(type, parent, options = {}) { + this.type = type; + if (type) this.#hasMagic = true; + this.#parent = parent; + this.#root = this.#parent ? this.#parent.#root : this; + this.#options = this.#root === this ? options : this.#root.#options; + this.#negs = this.#root === this ? [] : this.#root.#negs; + if (type === "!" && !this.#root.#filledNegs) this.#negs.push(this); + this.#parentIndex = this.#parent ? this.#parent.#parts.length : 0; + } + get hasMagic() { + if (this.#hasMagic !== void 0) return this.#hasMagic; + for (const p of this.#parts) { + if (typeof p === "string") continue; + if (p.type || p.hasMagic) return (this.#hasMagic = true); + } + return this.#hasMagic; + } + // reconstructs the pattern + toString() { + return this.#toString !== void 0 + ? this.#toString + : !this.type + ? (this.#toString = this.#parts.map(p => String(p)).join("")) + : (this.#toString = this.type + "(" + this.#parts.map(p => String(p)).join("|") + ")"); + } + #fillNegs() { + if (this !== this.#root) throw new Error("should only call on root"); + if (this.#filledNegs) return this; + this.toString(); + this.#filledNegs = true; + let n; + while ((n = this.#negs.pop())) { + if (n.type !== "!") continue; + let p = n; + let pp = p.#parent; + while (pp) { + for (let i = p.#parentIndex + 1; !pp.type && i < pp.#parts.length; i++) { + for (const part of n.#parts) { + if (typeof part === "string") { + throw new Error("string part in extglob AST??"); + } + part.copyIn(pp.#parts[i]); + } + } + p = pp; + pp = p.#parent; + } + } + return this; + } + push(...parts) { + for (const p of parts) { + if (p === "") continue; + if (typeof p !== "string" && !(p instanceof _a && p.#parent === this)) { + throw new Error("invalid part: " + p); + } + this.#parts.push(p); + } + } + toJSON() { + const ret = + this.type === null + ? this.#parts.slice().map(p => (typeof p === "string" ? p : p.toJSON())) + : [this.type, ...this.#parts.map(p => p.toJSON())]; + if (this.isStart() && !this.type) ret.unshift([]); + if (this.isEnd() && (this === this.#root || (this.#root.#filledNegs && this.#parent?.type === "!"))) { + ret.push({}); + } + return ret; + } + isStart() { + if (this.#root === this) return true; + if (!this.#parent?.isStart()) return false; + if (this.#parentIndex === 0) return true; + const p = this.#parent; + for (let i = 0; i < this.#parentIndex; i++) { + const pp = p.#parts[i]; + if (!(pp instanceof _a && pp.type === "!")) { + return false; + } + } + return true; + } + isEnd() { + if (this.#root === this) return true; + if (this.#parent?.type === "!") return true; + if (!this.#parent?.isEnd()) return false; + if (!this.type) return this.#parent?.isEnd(); + const pl = this.#parent ? this.#parent.#parts.length : 0; + return this.#parentIndex === pl - 1; + } + copyIn(part) { + if (typeof part === "string") this.push(part); + else this.push(part.clone(this)); + } + clone(parent) { + const c = new _a(this.type, parent); + for (const p of this.#parts) { + c.copyIn(p); + } + return c; + } + static #parseAST(str, ast, pos, opt, extDepth) { + const maxDepth = opt.maxExtglobRecursion ?? 2; + let escaping = false; + let inBrace = false; + let braceStart = -1; + let braceNeg = false; + if (ast.type === null) { + let i2 = pos; + let acc2 = ""; + while (i2 < str.length) { + const c = str.charAt(i2++); + if (escaping || c === "\\") { + escaping = !escaping; + acc2 += c; + continue; + } + if (inBrace) { + if (i2 === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i2 === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc2 += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i2; + braceNeg = false; + acc2 += c; + continue; + } + const doRecurse = !opt.noext && isExtglobType(c) && str.charAt(i2) === "(" && extDepth <= maxDepth; + if (doRecurse) { + ast.push(acc2); + acc2 = ""; + const ext2 = new _a(c, ast); + i2 = _a.#parseAST(str, ext2, i2, opt, extDepth + 1); + ast.push(ext2); + continue; + } + acc2 += c; + } + ast.push(acc2); + return i2; + } + let i = pos + 1; + let part = new _a(null, ast); + const parts = []; + let acc = ""; + while (i < str.length) { + const c = str.charAt(i++); + if (escaping || c === "\\") { + escaping = !escaping; + acc += c; + continue; + } + if (inBrace) { + if (i === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i; + braceNeg = false; + acc += c; + continue; + } + const doRecurse = + !opt.noext && + isExtglobType(c) && + str.charAt(i) === "(" /* c8 ignore start - the maxDepth is sufficient here */ && + (extDepth <= maxDepth || (ast && ast.#canAdoptType(c))); + if (doRecurse) { + const depthAdd = ast && ast.#canAdoptType(c) ? 0 : 1; + part.push(acc); + acc = ""; + const ext2 = new _a(c, part); + part.push(ext2); + i = _a.#parseAST(str, ext2, i, opt, extDepth + depthAdd); + continue; + } + if (c === "|") { + part.push(acc); + acc = ""; + parts.push(part); + part = new _a(null, ast); + continue; + } + if (c === ")") { + if (acc === "" && ast.#parts.length === 0) { + ast.#emptyExt = true; + } + part.push(acc); + acc = ""; + ast.push(...parts, part); + return i; + } + acc += c; + } + ast.type = null; + ast.#hasMagic = void 0; + ast.#parts = [str.substring(pos - 1)]; + return i; + } + #canAdoptWithSpace(child) { + return this.#canAdopt(child, adoptionWithSpaceMap); + } + #canAdopt(child, map = adoptionMap) { + if ( + !child || + typeof child !== "object" || + child.type !== null || + child.#parts.length !== 1 || + this.type === null + ) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canAdoptType(gc.type, map); + } + #canAdoptType(c, map = adoptionAnyMap) { + return !!map.get(this.type)?.includes(c); + } + #adoptWithSpace(child, index) { + const gc = child.#parts[0]; + const blank = new _a(null, gc, this.options); + blank.#parts.push(""); + gc.push(blank); + this.#adopt(child, index); + } + #adopt(child, index) { + const gc = child.#parts[0]; + this.#parts.splice(index, 1, ...gc.#parts); + for (const p of gc.#parts) { + if (typeof p === "object") p.#parent = this; + } + this.#toString = void 0; + } + #canUsurpType(c) { + const m = usurpMap.get(this.type); + return !!m?.has(c); + } + #canUsurp(child) { + if ( + !child || + typeof child !== "object" || + child.type !== null || + child.#parts.length !== 1 || + this.type === null || + this.#parts.length !== 1 + ) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canUsurpType(gc.type); + } + #usurp(child) { + const m = usurpMap.get(this.type); + const gc = child.#parts[0]; + const nt = m?.get(gc.type); + if (!nt) return false; + this.#parts = gc.#parts; + for (const p of this.#parts) { + if (typeof p === "object") { + p.#parent = this; + } + } + this.type = nt; + this.#toString = void 0; + this.#emptyExt = false; + } + static fromGlob(pattern, options = {}) { + const ast = new _a(null, void 0, options); + _a.#parseAST(pattern, ast, 0, options, 0); + return ast; + } + // returns the regular expression if there's magic, or the unescaped + // string if not. + toMMPattern() { + if (this !== this.#root) return this.#root.toMMPattern(); + const glob = this.toString(); + const [re, body, hasMagic, uflag] = this.toRegExpSource(); + const anyMagic = + hasMagic || + this.#hasMagic || + (this.#options.nocase && !this.#options.nocaseMagicOnly && glob.toUpperCase() !== glob.toLowerCase()); + if (!anyMagic) { + return body; + } + const flags = (this.#options.nocase ? "i" : "") + (uflag ? "u" : ""); + return Object.assign(new RegExp(`^${re}$`, flags), { + _src: re, + _glob: glob, + }); + } + get options() { + return this.#options; + } + // returns the string match, the regexp source, whether there's magic + // in the regexp (so a regular expression is required) and whether or + // not the uflag is needed for the regular expression (for posix classes) + // TODO: instead of injecting the start/end at this point, just return + // the BODY of the regexp, along with the start/end portions suitable + // for binding the start/end in either a joined full-path makeRe context + // (where we bind to (^|/), or a standalone matchPart context (where + // we bind to ^, and not /). Otherwise slashes get duped! + // + // In part-matching mode, the start is: + // - if not isStart: nothing + // - if traversal possible, but not allowed: ^(?!\.\.?$) + // - if dots allowed or not possible: ^ + // - if dots possible and not allowed: ^(?!\.) + // end is: + // - if not isEnd(): nothing + // - else: $ + // + // In full-path matching mode, we put the slash at the START of the + // pattern, so start is: + // - if first pattern: same as part-matching mode + // - if not isStart(): nothing + // - if traversal possible, but not allowed: /(?!\.\.?(?:$|/)) + // - if dots allowed or not possible: / + // - if dots possible and not allowed: /(?!\.) + // end is: + // - if last pattern, same as part-matching mode + // - else nothing + // + // Always put the (?:$|/) on negated tails, though, because that has to be + // there to bind the end of the negated pattern portion, and it's easier to + // just stick it in now rather than try to inject it later in the middle of + // the pattern. + // + // We can just always return the same end, and leave it up to the caller + // to know whether it's going to be used joined or in parts. + // And, if the start is adjusted slightly, can do the same there: + // - if not isStart: nothing + // - if traversal possible, but not allowed: (?:/|^)(?!\.\.?$) + // - if dots allowed or not possible: (?:/|^) + // - if dots possible and not allowed: (?:/|^)(?!\.) + // + // But it's better to have a simpler binding without a conditional, for + // performance, so probably better to return both start options. + // + // Then the caller just ignores the end if it's not the first pattern, + // and the start always gets applied. + // + // But that's always going to be $ if it's the ending pattern, or nothing, + // so the caller can just attach $ at the end of the pattern when building. + // + // So the todo is: + // - better detect what kind of start is needed + // - return both flavors of starting pattern + // - attach $ at the end of the pattern when creating the actual RegExp + // + // Ah, but wait, no, that all only applies to the root when the first pattern + // is not an extglob. If the first pattern IS an extglob, then we need all + // that dot prevention biz to live in the extglob portions, because eg + // +(*|.x*) can match .xy but not .yx. + // + // So, return the two flavors if it's #root and the first child is not an + // AST, otherwise leave it to the child AST to handle it, and there, + // use the (?:^|/) style of start binding. + // + // Even simplified further: + // - Since the start for a join is eg /(?!\.) and the start for a part + // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root + // or start or whatever) and prepend ^ or / at the Regexp construction. + toRegExpSource(allowDot) { + const dot = allowDot ?? !!this.#options.dot; + if (this.#root === this) { + this.#flatten(); + this.#fillNegs(); + } + if (!isExtglobAST(this)) { + const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some(s => typeof s !== "string"); + const src = this.#parts + .map(p => { + const [re, _, hasMagic, uflag] = + typeof p === "string" ? _a.#parseGlob(p, this.#hasMagic, noEmpty) : p.toRegExpSource(allowDot); + this.#hasMagic = this.#hasMagic || hasMagic; + this.#uflag = this.#uflag || uflag; + return re; + }) + .join(""); + let start2 = ""; + if (this.isStart()) { + if (typeof this.#parts[0] === "string") { + const dotTravAllowed = this.#parts.length === 1 && justDots.has(this.#parts[0]); + if (!dotTravAllowed) { + const aps = addPatternStart; + const needNoTrav = + // dots are allowed, and the pattern starts with [ or . + (dot && aps.has(src.charAt(0))) || // the pattern starts with \., and then [ or . + (src.startsWith("\\.") && aps.has(src.charAt(2))) || // the pattern starts with \.\., and then [ or . + (src.startsWith("\\.\\.") && aps.has(src.charAt(4))); + const needNoDot = !dot && !allowDot && aps.has(src.charAt(0)); + start2 = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : ""; + } + } + } + let end = ""; + if (this.isEnd() && this.#root.#filledNegs && this.#parent?.type === "!") { + end = "(?:$|\\/)"; + } + const final2 = start2 + src + end; + return [final2, (0, unescape_js_12.unescape)(src), (this.#hasMagic = !!this.#hasMagic), this.#uflag]; + } + const repeated = this.type === "*" || this.type === "+"; + const start = this.type === "!" ? "(?:(?!(?:" : "(?:"; + let body = this.#partsToRegExp(dot); + if (this.isStart() && this.isEnd() && !body && this.type !== "!") { + const s = this.toString(); + const me = this; + me.#parts = [s]; + me.type = null; + me.#hasMagic = void 0; + return [s, (0, unescape_js_12.unescape)(this.toString()), false, false]; + } + let bodyDotAllowed = !repeated || allowDot || dot || !startNoDot ? "" : this.#partsToRegExp(true); + if (bodyDotAllowed === body) { + bodyDotAllowed = ""; + } + if (bodyDotAllowed) { + body = `(?:${body})(?:${bodyDotAllowed})*?`; + } + let final = ""; + if (this.type === "!" && this.#emptyExt) { + final = (this.isStart() && !dot ? startNoDot : "") + starNoEmpty; + } else { + const close = + this.type === "!" + ? // !() must match something,but !(x) can match '' + "))" + (this.isStart() && !dot && !allowDot ? startNoDot : "") + star2 + ")" + : this.type === "@" + ? ")" + : this.type === "?" + ? ")?" + : this.type === "+" && bodyDotAllowed + ? ")" + : this.type === "*" && bodyDotAllowed + ? `)?` + : `)${this.type}`; + final = start + body + close; + } + return [final, (0, unescape_js_12.unescape)(body), (this.#hasMagic = !!this.#hasMagic), this.#uflag]; + } + #flatten() { + if (!isExtglobAST(this)) { + for (const p of this.#parts) { + if (typeof p === "object") { + p.#flatten(); + } + } + } else { + let iterations = 0; + let done = false; + do { + done = true; + for (let i = 0; i < this.#parts.length; i++) { + const c = this.#parts[i]; + if (typeof c === "object") { + c.#flatten(); + if (this.#canAdopt(c)) { + done = false; + this.#adopt(c, i); + } else if (this.#canAdoptWithSpace(c)) { + done = false; + this.#adoptWithSpace(c, i); + } else if (this.#canUsurp(c)) { + done = false; + this.#usurp(c); + } + } + } + } while (!done && ++iterations < 10); + } + this.#toString = void 0; + } + #partsToRegExp(dot) { + return this.#parts + .map(p => { + if (typeof p === "string") { + throw new Error("string type in extglob ast??"); + } + const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot); + this.#uflag = this.#uflag || uflag; + return re; + }) + .filter(p => !(this.isStart() && this.isEnd()) || !!p) + .join("|"); + } + static #parseGlob(glob, hasMagic, noEmpty = false) { + let escaping = false; + let re = ""; + let uflag = false; + let inStar = false; + for (let i = 0; i < glob.length; i++) { + const c = glob.charAt(i); + if (escaping) { + escaping = false; + re += (reSpecials.has(c) ? "\\" : "") + c; + continue; + } + if (c === "*") { + if (inStar) continue; + inStar = true; + re += noEmpty && /^[*]+$/.test(glob) ? starNoEmpty : star2; + hasMagic = true; + continue; + } else { + inStar = false; + } + if (c === "\\") { + if (i === glob.length - 1) { + re += "\\\\"; + } else { + escaping = true; + } + continue; + } + if (c === "[") { + const [src, needUflag, consumed, magic] = (0, brace_expressions_js_1.parseClass)(glob, i); + if (consumed) { + re += src; + uflag = uflag || needUflag; + i += consumed - 1; + hasMagic = hasMagic || magic; + continue; + } + } + if (c === "?") { + re += qmark2; + hasMagic = true; + continue; + } + re += regExpEscape2(c); + } + return [re, (0, unescape_js_12.unescape)(glob), !!hasMagic, uflag]; + } + }; + exports2.AST = AST; + _a = AST; + }, + }); + + // dist/commonjs/escape.js + var require_escape = __commonJS({ + "dist/commonjs/escape.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.escape = void 0; + var escape = (s, { windowsPathsNoEscape = false, magicalBraces = false } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape ? s.replace(/[?*()[\]{}]/g, "[$&]") : s.replace(/[?*()[\]\\{}]/g, "\\$&"); + } + return windowsPathsNoEscape ? s.replace(/[?*()[\]]/g, "[$&]") : s.replace(/[?*()[\]\\]/g, "\\$&"); + }; + exports2.escape = escape; + }, + }); + + // dist/commonjs/index.js + Object.defineProperty(exports, "__esModule", { value: true }); + exports.unescape = + exports.escape = + exports.AST = + exports.Minimatch = + exports.match = + exports.makeRe = + exports.braceExpand = + exports.defaults = + exports.filter = + exports.GLOBSTAR = + exports.sep = + exports.minimatch = + void 0; + var brace_expansion_1 = require_commonjs2(); + var assert_valid_pattern_js_1 = require_assert_valid_pattern(); + var ast_js_1 = require_ast(); + var escape_js_1 = require_escape(); + var unescape_js_1 = require_unescape(); + var minimatch = (p, pattern, options = {}) => { + (0, assert_valid_pattern_js_1.assertValidPattern)(pattern); + if (!options.nocomment && pattern.charAt(0) === "#") { + return false; + } + return new Minimatch(pattern, options).match(p); + }; + exports.minimatch = minimatch; + var starDotExtRE = /^\*+([^+@!?*[(]*)$/; + var starDotExtTest = ext2 => f => !f.startsWith(".") && f.endsWith(ext2); + var starDotExtTestDot = ext2 => f => f.endsWith(ext2); + var starDotExtTestNocase = ext2 => { + ext2 = ext2.toLowerCase(); + return f => !f.startsWith(".") && f.toLowerCase().endsWith(ext2); + }; + var starDotExtTestNocaseDot = ext2 => { + ext2 = ext2.toLowerCase(); + return f => f.toLowerCase().endsWith(ext2); + }; + var starDotStarRE = /^\*+\.\*+$/; + var starDotStarTest = f => !f.startsWith(".") && f.includes("."); + var starDotStarTestDot = f => f !== "." && f !== ".." && f.includes("."); + var dotStarRE = /^\.\*+$/; + var dotStarTest = f => f !== "." && f !== ".." && f.startsWith("."); + var starRE = /^\*+$/; + var starTest = f => f.length !== 0 && !f.startsWith("."); + var starTestDot = f => f.length !== 0 && f !== "." && f !== ".."; + var qmarksRE = /^\?+([^+@!?*[(]*)?$/; + var qmarksTestNocase = ([m0, ext2 = ""]) => { + const noext = qmarksTestNoExt([m0]); + if (!ext2) return noext; + ext2 = ext2.toLowerCase(); + return f => noext(f) && f.toLowerCase().endsWith(ext2); + }; + var qmarksTestNocaseDot = ([m0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([m0]); + if (!ext2) return noext; + ext2 = ext2.toLowerCase(); + return f => noext(f) && f.toLowerCase().endsWith(ext2); + }; + var qmarksTestDot = ([m0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([m0]); + return !ext2 ? noext : f => noext(f) && f.endsWith(ext2); + }; + var qmarksTest = ([m0, ext2 = ""]) => { + const noext = qmarksTestNoExt([m0]); + return !ext2 ? noext : f => noext(f) && f.endsWith(ext2); + }; + var qmarksTestNoExt = ([m0]) => { + const len = m0.length; + return f => f.length === len && !f.startsWith("."); + }; + var qmarksTestNoExtDot = ([m0]) => { + const len = m0.length; + return f => f.length === len && f !== "." && f !== ".."; + }; + var defaultPlatform = + typeof process === "object" && process + ? (typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__) || + process.platform + : "posix"; + var path = { + win32: { sep: "\\" }, + posix: { sep: "/" }, + }; + exports.sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep; + exports.minimatch.sep = exports.sep; + exports.GLOBSTAR = /* @__PURE__ */ Symbol("globstar **"); + exports.minimatch.GLOBSTAR = exports.GLOBSTAR; + var qmark = "[^/]"; + var star = qmark + "*?"; + var twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"; + var twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?"; + var filter = + (pattern, options = {}) => + p => + (0, exports.minimatch)(p, pattern, options); + exports.filter = filter; + exports.minimatch.filter = exports.filter; + var ext = (a, b = {}) => Object.assign({}, a, b); + var defaults = def => { + if (!def || typeof def !== "object" || !Object.keys(def).length) { + return exports.minimatch; + } + const orig = exports.minimatch; + const m = (p, pattern, options = {}) => orig(p, pattern, ext(def, options)); + return Object.assign(m, { + Minimatch: class Minimatch extends orig.Minimatch { + constructor(pattern, options = {}) { + super(pattern, ext(def, options)); + } + static defaults(options) { + return orig.defaults(ext(def, options)).Minimatch; + } + }, + AST: class AST extends orig.AST { + /* c8 ignore start */ + constructor(type, parent, options = {}) { + super(type, parent, ext(def, options)); + } + /* c8 ignore stop */ + static fromGlob(pattern, options = {}) { + return orig.AST.fromGlob(pattern, ext(def, options)); + } + }, + unescape: (s, options = {}) => orig.unescape(s, ext(def, options)), + escape: (s, options = {}) => orig.escape(s, ext(def, options)), + filter: (pattern, options = {}) => orig.filter(pattern, ext(def, options)), + defaults: options => orig.defaults(ext(def, options)), + makeRe: (pattern, options = {}) => orig.makeRe(pattern, ext(def, options)), + braceExpand: (pattern, options = {}) => orig.braceExpand(pattern, ext(def, options)), + match: (list, pattern, options = {}) => orig.match(list, pattern, ext(def, options)), + sep: orig.sep, + GLOBSTAR: exports.GLOBSTAR, + }); + }; + exports.defaults = defaults; + exports.minimatch.defaults = exports.defaults; + var braceExpand = (pattern, options = {}) => { + (0, assert_valid_pattern_js_1.assertValidPattern)(pattern); + if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) { + return [pattern]; + } + return (0, brace_expansion_1.expand)(pattern, { max: options.braceExpandMax }); + }; + exports.braceExpand = braceExpand; + exports.minimatch.braceExpand = exports.braceExpand; + var makeRe = (pattern, options = {}) => new Minimatch(pattern, options).makeRe(); + exports.makeRe = makeRe; + exports.minimatch.makeRe = exports.makeRe; + var match = (list, pattern, options = {}) => { + const mm = new Minimatch(pattern, options); + list = list.filter(f => mm.match(f)); + if (mm.options.nonull && !list.length) { + list.push(pattern); + } + return list; + }; + exports.match = match; + exports.minimatch.match = exports.match; + var globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/; + var regExpEscape = s => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + var Minimatch = class { + options; + set; + pattern; + windowsPathsNoEscape; + nonegate; + negate; + comment; + empty; + preserveMultipleSlashes; + partial; + globSet; + globParts; + nocase; + isWindows; + platform; + windowsNoMagicRoot; + maxGlobstarRecursion; + regexp; + constructor(pattern, options = {}) { + (0, assert_valid_pattern_js_1.assertValidPattern)(pattern); + options = options || {}; + this.options = options; + this.maxGlobstarRecursion = options.maxGlobstarRecursion ?? 200; + this.pattern = pattern; + this.platform = options.platform || defaultPlatform; + this.isWindows = this.platform === "win32"; + const awe = "allowWindowsEscape"; + this.windowsPathsNoEscape = !!options.windowsPathsNoEscape || options[awe] === false; + if (this.windowsPathsNoEscape) { + this.pattern = this.pattern.replace(/\\/g, "/"); + } + this.preserveMultipleSlashes = !!options.preserveMultipleSlashes; + this.regexp = null; + this.negate = false; + this.nonegate = !!options.nonegate; + this.comment = false; + this.empty = false; + this.partial = !!options.partial; + this.nocase = !!this.options.nocase; + this.windowsNoMagicRoot = + options.windowsNoMagicRoot !== void 0 ? options.windowsNoMagicRoot : !!(this.isWindows && this.nocase); + this.globSet = []; + this.globParts = []; + this.set = []; + this.make(); + } + hasMagic() { + if (this.options.magicalBraces && this.set.length > 1) { + return true; + } + for (const pattern of this.set) { + for (const part of pattern) { + if (typeof part !== "string") return true; + } + } + return false; + } + debug(..._) {} + make() { + const pattern = this.pattern; + const options = this.options; + if (!options.nocomment && pattern.charAt(0) === "#") { + this.comment = true; + return; + } + if (!pattern) { + this.empty = true; + return; + } + this.parseNegate(); + this.globSet = [...new Set(this.braceExpand())]; + if (options.debug) { + this.debug = (...args) => console.error(...args); + } + this.debug(this.pattern, this.globSet); + const rawGlobParts = this.globSet.map(s => this.slashSplit(s)); + this.globParts = this.preprocess(rawGlobParts); + this.debug(this.pattern, this.globParts); + let set = this.globParts.map((s, _, __) => { + if (this.isWindows && this.windowsNoMagicRoot) { + const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]); + const isDrive = /^[a-z]:/i.test(s[0]); + if (isUNC) { + return [...s.slice(0, 4), ...s.slice(4).map(ss => this.parse(ss))]; + } else if (isDrive) { + return [s[0], ...s.slice(1).map(ss => this.parse(ss))]; + } + } + return s.map(ss => this.parse(ss)); + }); + this.debug(this.pattern, set); + this.set = set.filter(s => s.indexOf(false) === -1); + if (this.isWindows) { + for (let i = 0; i < this.set.length; i++) { + const p = this.set[i]; + if ( + p[0] === "" && + p[1] === "" && + this.globParts[i][2] === "?" && + typeof p[3] === "string" && + /^[a-z]:$/i.test(p[3]) + ) { + p[2] = "?"; + } + } + } + this.debug(this.pattern, this.set); + } + // various transforms to equivalent pattern sets that are + // faster to process in a filesystem walk. The goal is to + // eliminate what we can, and push all ** patterns as far + // to the right as possible, even if it increases the number + // of patterns that we have to process. + preprocess(globParts) { + if (this.options.noglobstar) { + for (const partset of globParts) { + for (let j = 0; j < partset.length; j++) { + if (partset[j] === "**") { + partset[j] = "*"; + } + } + } + } + const { optimizationLevel = 1 } = this.options; + if (optimizationLevel >= 2) { + globParts = this.firstPhasePreProcess(globParts); + globParts = this.secondPhasePreProcess(globParts); + } else if (optimizationLevel >= 1) { + globParts = this.levelOneOptimize(globParts); + } else { + globParts = this.adjascentGlobstarOptimize(globParts); + } + return globParts; + } + // just get rid of adjascent ** portions + adjascentGlobstarOptimize(globParts) { + return globParts.map(parts => { + let gs = -1; + while (-1 !== (gs = parts.indexOf("**", gs + 1))) { + let i = gs; + while (parts[i + 1] === "**") { + i++; + } + if (i !== gs) { + parts.splice(gs, i - gs); + } + } + return parts; + }); + } + // get rid of adjascent ** and resolve .. portions + levelOneOptimize(globParts) { + return globParts.map(parts => { + parts = parts.reduce((set, part) => { + const prev = set[set.length - 1]; + if (part === "**" && prev === "**") { + return set; + } + if (part === "..") { + if (prev && prev !== ".." && prev !== "." && prev !== "**") { + set.pop(); + return set; + } + } + set.push(part); + return set; + }, []); + return parts.length === 0 ? [""] : parts; + }); + } + levelTwoFileOptimize(parts) { + if (!Array.isArray(parts)) { + parts = this.slashSplit(parts); + } + let didSomething = false; + do { + didSomething = false; + if (!this.preserveMultipleSlashes) { + for (let i = 1; i < parts.length - 1; i++) { + const p = parts[i]; + if (i === 1 && p === "" && parts[0] === "") continue; + if (p === "." || p === "") { + didSomething = true; + parts.splice(i, 1); + i--; + } + } + if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) { + didSomething = true; + parts.pop(); + } + } + let dd = 0; + while (-1 !== (dd = parts.indexOf("..", dd + 1))) { + const p = parts[dd - 1]; + if (p && p !== "." && p !== ".." && p !== "**" && !(this.isWindows && /^[a-z]:$/i.test(p))) { + didSomething = true; + parts.splice(dd - 1, 2); + dd -= 2; + } + } + } while (didSomething); + return parts.length === 0 ? [""] : parts; + } + // First phase: single-pattern processing + //
 is 1 or more portions
+    //  is 1 or more portions
+    // 

is any portion other than ., .., '', or ** + // is . or '' + // + // **/.. is *brutal* for filesystem walking performance, because + // it effectively resets the recursive walk each time it occurs, + // and ** cannot be reduced out by a .. pattern part like a regexp + // or most strings (other than .., ., and '') can be. + // + //

/**/../

/

/ -> {

/../

/

/,

/**/

/

/} + //

// -> 
/
+    // 
/

/../ ->

/
+    // **/**/ -> **/
+    //
+    // **/*/ -> */**/ <== not valid because ** doesn't follow
+    // this WOULD be allowed if ** did follow symlinks, or * didn't
+    firstPhasePreProcess(globParts) {
+      let didSomething = false;
+      do {
+        didSomething = false;
+        for (let parts of globParts) {
+          let gs = -1;
+          while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
+            let gss = gs;
+            while (parts[gss + 1] === "**") {
+              gss++;
+            }
+            if (gss > gs) {
+              parts.splice(gs + 1, gss - gs);
+            }
+            let next = parts[gs + 1];
+            const p = parts[gs + 2];
+            const p2 = parts[gs + 3];
+            if (next !== "..") continue;
+            if (!p || p === "." || p === ".." || !p2 || p2 === "." || p2 === "..") {
+              continue;
+            }
+            didSomething = true;
+            parts.splice(gs, 1);
+            const other = parts.slice(0);
+            other[gs] = "**";
+            globParts.push(other);
+            gs--;
+          }
+          if (!this.preserveMultipleSlashes) {
+            for (let i = 1; i < parts.length - 1; i++) {
+              const p = parts[i];
+              if (i === 1 && p === "" && parts[0] === "") continue;
+              if (p === "." || p === "") {
+                didSomething = true;
+                parts.splice(i, 1);
+                i--;
+              }
+            }
+            if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
+              didSomething = true;
+              parts.pop();
+            }
+          }
+          let dd = 0;
+          while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
+            const p = parts[dd - 1];
+            if (p && p !== "." && p !== ".." && p !== "**") {
+              didSomething = true;
+              const needDot = dd === 1 && parts[dd + 1] === "**";
+              const splin = needDot ? ["."] : [];
+              parts.splice(dd - 1, 2, ...splin);
+              if (parts.length === 0) parts.push("");
+              dd -= 2;
+            }
+          }
+        }
+      } while (didSomething);
+      return globParts;
+    }
+    // second phase: multi-pattern dedupes
+    // {
/*/,
/

/} ->

/*/
+    // {
/,
/} -> 
/
+    // {
/**/,
/} -> 
/**/
+    //
+    // {
/**/,
/**/

/} ->

/**/
+    // ^-- not valid because ** doens't follow symlinks
+    secondPhasePreProcess(globParts) {
+      for (let i = 0; i < globParts.length - 1; i++) {
+        for (let j = i + 1; j < globParts.length; j++) {
+          const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);
+          if (matched) {
+            globParts[i] = [];
+            globParts[j] = matched;
+            break;
+          }
+        }
+      }
+      return globParts.filter(gs => gs.length);
+    }
+    partsMatch(a, b, emptyGSMatch = false) {
+      let ai = 0;
+      let bi = 0;
+      let result = [];
+      let which = "";
+      while (ai < a.length && bi < b.length) {
+        if (a[ai] === b[bi]) {
+          result.push(which === "b" ? b[bi] : a[ai]);
+          ai++;
+          bi++;
+        } else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) {
+          result.push(a[ai]);
+          ai++;
+        } else if (emptyGSMatch && b[bi] === "**" && a[ai] === b[bi + 1]) {
+          result.push(b[bi]);
+          bi++;
+        } else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") {
+          if (which === "b") return false;
+          which = "a";
+          result.push(a[ai]);
+          ai++;
+          bi++;
+        } else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") {
+          if (which === "a") return false;
+          which = "b";
+          result.push(b[bi]);
+          ai++;
+          bi++;
+        } else {
+          return false;
+        }
+      }
+      return a.length === b.length && result;
+    }
+    parseNegate() {
+      if (this.nonegate) return;
+      const pattern = this.pattern;
+      let negate = false;
+      let negateOffset = 0;
+      for (let i = 0; i < pattern.length && pattern.charAt(i) === "!"; i++) {
+        negate = !negate;
+        negateOffset++;
+      }
+      if (negateOffset) this.pattern = pattern.slice(negateOffset);
+      this.negate = negate;
+    }
+    // set partial to true to test if, for example,
+    // "/a/b" matches the start of "/*/b/*/d"
+    // Partial means, if you run out of file before you run
+    // out of pattern, then that's fine, as long as all
+    // the parts match.
+    matchOne(file, pattern, partial = false) {
+      let fileStartIndex = 0;
+      let patternStartIndex = 0;
+      if (this.isWindows) {
+        const fileDrive = typeof file[0] === "string" && /^[a-z]:$/i.test(file[0]);
+        const fileUNC = !fileDrive && file[0] === "" && file[1] === "" && file[2] === "?" && /^[a-z]:$/i.test(file[3]);
+        const patternDrive = typeof pattern[0] === "string" && /^[a-z]:$/i.test(pattern[0]);
+        const patternUNC =
+          !patternDrive &&
+          pattern[0] === "" &&
+          pattern[1] === "" &&
+          pattern[2] === "?" &&
+          typeof pattern[3] === "string" &&
+          /^[a-z]:$/i.test(pattern[3]);
+        const fdi = fileUNC ? 3 : fileDrive ? 0 : void 0;
+        const pdi = patternUNC ? 3 : patternDrive ? 0 : void 0;
+        if (typeof fdi === "number" && typeof pdi === "number") {
+          const [fd, pd] = [file[fdi], pattern[pdi]];
+          if (fd.toLowerCase() === pd.toLowerCase()) {
+            pattern[pdi] = fd;
+            patternStartIndex = pdi;
+            fileStartIndex = fdi;
+          }
+        }
+      }
+      const { optimizationLevel = 1 } = this.options;
+      if (optimizationLevel >= 2) {
+        file = this.levelTwoFileOptimize(file);
+      }
+      if (pattern.includes(exports.GLOBSTAR)) {
+        return this.#matchGlobstar(file, pattern, partial, fileStartIndex, patternStartIndex);
+      }
+      return this.#matchOne(file, pattern, partial, fileStartIndex, patternStartIndex);
+    }
+    #matchGlobstar(file, pattern, partial, fileIndex, patternIndex) {
+      const firstgs = pattern.indexOf(exports.GLOBSTAR, patternIndex);
+      const lastgs = pattern.lastIndexOf(exports.GLOBSTAR);
+      const [head, body, tail] = partial
+        ? [pattern.slice(patternIndex, firstgs), pattern.slice(firstgs + 1), []]
+        : [pattern.slice(patternIndex, firstgs), pattern.slice(firstgs + 1, lastgs), pattern.slice(lastgs + 1)];
+      if (head.length) {
+        const fileHead = file.slice(fileIndex, fileIndex + head.length);
+        if (!this.#matchOne(fileHead, head, partial, 0, 0)) {
+          return false;
+        }
+        fileIndex += head.length;
+        patternIndex += head.length;
+      }
+      let fileTailMatch = 0;
+      if (tail.length) {
+        if (tail.length + fileIndex > file.length) return false;
+        let tailStart = file.length - tail.length;
+        if (this.#matchOne(file, tail, partial, tailStart, 0)) {
+          fileTailMatch = tail.length;
+        } else {
+          if (file[file.length - 1] !== "" || fileIndex + tail.length === file.length) {
+            return false;
+          }
+          tailStart--;
+          if (!this.#matchOne(file, tail, partial, tailStart, 0)) {
+            return false;
+          }
+          fileTailMatch = tail.length + 1;
+        }
+      }
+      if (!body.length) {
+        let sawSome = !!fileTailMatch;
+        for (let i2 = fileIndex; i2 < file.length - fileTailMatch; i2++) {
+          const f = String(file[i2]);
+          sawSome = true;
+          if (f === "." || f === ".." || (!this.options.dot && f.startsWith("."))) {
+            return false;
+          }
+        }
+        return partial || sawSome;
+      }
+      const bodySegments = [[[], 0]];
+      let currentBody = bodySegments[0];
+      let nonGsParts = 0;
+      const nonGsPartsSums = [0];
+      for (const b of body) {
+        if (b === exports.GLOBSTAR) {
+          nonGsPartsSums.push(nonGsParts);
+          currentBody = [[], 0];
+          bodySegments.push(currentBody);
+        } else {
+          currentBody[0].push(b);
+          nonGsParts++;
+        }
+      }
+      let i = bodySegments.length - 1;
+      const fileLength = file.length - fileTailMatch;
+      for (const b of bodySegments) {
+        b[1] = fileLength - (nonGsPartsSums[i--] + b[0].length);
+      }
+      return !!this.#matchGlobStarBodySections(file, bodySegments, fileIndex, 0, partial, 0, !!fileTailMatch);
+    }
+    // return false for "nope, not matching"
+    // return null for "not matching, cannot keep trying"
+    #matchGlobStarBodySections(file, bodySegments, fileIndex, bodyIndex, partial, globStarDepth, sawTail) {
+      const bs = bodySegments[bodyIndex];
+      if (!bs) {
+        for (let i = fileIndex; i < file.length; i++) {
+          sawTail = true;
+          const f = file[i];
+          if (f === "." || f === ".." || (!this.options.dot && f.startsWith("."))) {
+            return false;
+          }
+        }
+        return sawTail;
+      }
+      const [body, after] = bs;
+      while (fileIndex <= after) {
+        const m = this.#matchOne(file.slice(0, fileIndex + body.length), body, partial, fileIndex, 0);
+        if (m && globStarDepth < this.maxGlobstarRecursion) {
+          const sub = this.#matchGlobStarBodySections(
+            file,
+            bodySegments,
+            fileIndex + body.length,
+            bodyIndex + 1,
+            partial,
+            globStarDepth + 1,
+            sawTail,
+          );
+          if (sub !== false) {
+            return sub;
+          }
+        }
+        const f = file[fileIndex];
+        if (f === "." || f === ".." || (!this.options.dot && f.startsWith("."))) {
+          return false;
+        }
+        fileIndex++;
+      }
+      return partial || null;
+    }
+    #matchOne(file, pattern, partial, fileIndex, patternIndex) {
+      let fi;
+      let pi;
+      let pl;
+      let fl;
+      for (fi = fileIndex, pi = patternIndex, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
+        this.debug("matchOne loop");
+        let p = pattern[pi];
+        let f = file[fi];
+        this.debug(pattern, p, f);
+        if (p === false || p === exports.GLOBSTAR) {
+          return false;
+        }
+        let hit;
+        if (typeof p === "string") {
+          hit = f === p;
+          this.debug("string match", p, f, hit);
+        } else {
+          hit = p.test(f);
+          this.debug("pattern match", p, f, hit);
+        }
+        if (!hit) return false;
+      }
+      if (fi === fl && pi === pl) {
+        return true;
+      } else if (fi === fl) {
+        return partial;
+      } else if (pi === pl) {
+        return fi === fl - 1 && file[fi] === "";
+      } else {
+        throw new Error("wtf?");
+      }
+    }
+    braceExpand() {
+      return (0, exports.braceExpand)(this.pattern, this.options);
+    }
+    parse(pattern) {
+      (0, assert_valid_pattern_js_1.assertValidPattern)(pattern);
+      const options = this.options;
+      if (pattern === "**") return exports.GLOBSTAR;
+      if (pattern === "") return "";
+      let m;
+      let fastTest = null;
+      if ((m = pattern.match(starRE))) {
+        fastTest = options.dot ? starTestDot : starTest;
+      } else if ((m = pattern.match(starDotExtRE))) {
+        fastTest = (
+          options.nocase
+            ? options.dot
+              ? starDotExtTestNocaseDot
+              : starDotExtTestNocase
+            : options.dot
+              ? starDotExtTestDot
+              : starDotExtTest
+        )(m[1]);
+      } else if ((m = pattern.match(qmarksRE))) {
+        fastTest = (
+          options.nocase
+            ? options.dot
+              ? qmarksTestNocaseDot
+              : qmarksTestNocase
+            : options.dot
+              ? qmarksTestDot
+              : qmarksTest
+        )(m);
+      } else if ((m = pattern.match(starDotStarRE))) {
+        fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
+      } else if ((m = pattern.match(dotStarRE))) {
+        fastTest = dotStarTest;
+      }
+      const re = ast_js_1.AST.fromGlob(pattern, this.options).toMMPattern();
+      if (fastTest && typeof re === "object") {
+        Reflect.defineProperty(re, "test", { value: fastTest });
+      }
+      return re;
+    }
+    makeRe() {
+      if (this.regexp || this.regexp === false) return this.regexp;
+      const set = this.set;
+      if (!set.length) {
+        this.regexp = false;
+        return this.regexp;
+      }
+      const options = this.options;
+      const twoStar = options.noglobstar ? star : options.dot ? twoStarDot : twoStarNoDot;
+      const flags = new Set(options.nocase ? ["i"] : []);
+      let re = set
+        .map(pattern => {
+          const pp = pattern.map(p => {
+            if (p instanceof RegExp) {
+              for (const f of p.flags.split("")) flags.add(f);
+            }
+            return typeof p === "string" ? regExpEscape(p) : p === exports.GLOBSTAR ? exports.GLOBSTAR : p._src;
+          });
+          pp.forEach((p, i) => {
+            const next = pp[i + 1];
+            const prev = pp[i - 1];
+            if (p !== exports.GLOBSTAR || prev === exports.GLOBSTAR) {
+              return;
+            }
+            if (prev === void 0) {
+              if (next !== void 0 && next !== exports.GLOBSTAR) {
+                pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
+              } else {
+                pp[i] = twoStar;
+              }
+            } else if (next === void 0) {
+              pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + ")?";
+            } else if (next !== exports.GLOBSTAR) {
+              pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next;
+              pp[i + 1] = exports.GLOBSTAR;
+            }
+          });
+          const filtered = pp.filter(p => p !== exports.GLOBSTAR);
+          if (this.partial && filtered.length >= 1) {
+            const prefixes = [];
+            for (let i = 1; i <= filtered.length; i++) {
+              prefixes.push(filtered.slice(0, i).join("/"));
+            }
+            return "(?:" + prefixes.join("|") + ")";
+          }
+          return filtered.join("/");
+        })
+        .join("|");
+      const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
+      re = "^" + open + re + close + "$";
+      if (this.partial) {
+        re = "^(?:\\/|" + open + re.slice(1, -1) + close + ")$";
+      }
+      if (this.negate) re = "^(?!" + re + ").+$";
+      try {
+        this.regexp = new RegExp(re, [...flags].join(""));
+      } catch {
+        this.regexp = false;
+      }
+      return this.regexp;
+    }
+    slashSplit(p) {
+      if (this.preserveMultipleSlashes) {
+        return p.split("/");
+      } else if (this.isWindows && /^\/\/[^/]+/.test(p)) {
+        return ["", ...p.split(/\/+/)];
+      } else {
+        return p.split(/\/+/);
+      }
+    }
+    match(f, partial = this.partial) {
+      this.debug("match", f, this.pattern);
+      if (this.comment) {
+        return false;
+      }
+      if (this.empty) {
+        return f === "";
+      }
+      if (f === "/" && partial) {
+        return true;
+      }
+      const options = this.options;
+      if (this.isWindows) {
+        f = f.split("\\").join("/");
+      }
+      const ff = this.slashSplit(f);
+      this.debug(this.pattern, "split", ff);
+      const set = this.set;
+      this.debug(this.pattern, "set", set);
+      let filename = ff[ff.length - 1];
+      if (!filename) {
+        for (let i = ff.length - 2; !filename && i >= 0; i--) {
+          filename = ff[i];
+        }
+      }
+      for (const pattern of set) {
+        let file = ff;
+        if (options.matchBase && pattern.length === 1) {
+          file = [filename];
+        }
+        const hit = this.matchOne(file, pattern, partial);
+        if (hit) {
+          if (options.flipNegate) {
+            return true;
+          }
+          return !this.negate;
+        }
+      }
+      if (options.flipNegate) {
+        return false;
+      }
+      return this.negate;
+    }
+    static defaults(def) {
+      return exports.minimatch.defaults(def).Minimatch;
+    }
+  };
+  exports.Minimatch = Minimatch;
+  var ast_js_2 = require_ast();
+  Object.defineProperty(exports, "AST", {
+    enumerable: true,
+    get: function () {
+      return ast_js_2.AST;
+    },
+  });
+  var escape_js_2 = require_escape();
+  Object.defineProperty(exports, "escape", {
+    enumerable: true,
+    get: function () {
+      return escape_js_2.escape;
+    },
+  });
+  var unescape_js_2 = require_unescape();
+  Object.defineProperty(exports, "unescape", {
+    enumerable: true,
+    get: function () {
+      return unescape_js_2.unescape;
+    },
+  });
+  exports.minimatch.AST = ast_js_1.AST;
+  exports.minimatch.Minimatch = Minimatch;
+  exports.minimatch.escape = escape_js_1.escape;
+  exports.minimatch.unescape = unescape_js_1.unescape;
 
-export default { glob, globSync };
+  // --- end vendored minimatch ---
+  return (_minimatch = module.exports);
+}
diff --git a/src/js/internal/fs/watch.ts b/src/js/internal/fs/watch.ts
index 1ba007d508ff..0b748ab51f17 100644
--- a/src/js/internal/fs/watch.ts
+++ b/src/js/internal/fs/watch.ts
@@ -1,12 +1,107 @@
 // fs.watch is lazily loaded so the FSWatcher class is only set up when it is used.
 const EventEmitter = require("node:events");
+const { basename } = require("node:path");
 
 // The native `node:fs` binding, shared via `internal/fs/binding`.
 const fs = require("internal/fs/binding");
 
+// Creates an ignore matcher function from the `ignore` watch option,
+// mirroring node lib/internal/fs/watchers.js createIgnoreMatcher.
+// string -> glob (patterns without a slash also match the basename),
+// RegExp -> exec, function -> called with the filename. Arrays compose.
+function makeGlobMatcher(glob) {
+  return function matchGlob(filename) {
+    return glob.match(filename);
+  };
+}
+function makeGlobOrBasenameMatcher(glob) {
+  return function matchGlobOrBasename(filename) {
+    return glob.match(filename) || glob.match(basename(filename));
+  };
+}
+function makeRegexMatcher(matcher) {
+  return function matchRegex(filename) {
+    return matcher.exec(filename) !== null;
+  };
+}
+
+function createIgnoreMatcher(ignore) {
+  if (ignore == null) return null;
+  const matchers = $isArray(ignore) ? ignore : [ignore];
+  const compiled: Array<(filename: string) => boolean> = [];
+
+  for (const matcher of matchers) {
+    if (typeof matcher === "string") {
+      if (matcher.length === 0) {
+        throw $ERR_INVALID_ARG_VALUE("options.ignore", matcher, "must not be empty");
+      }
+      const glob = new Bun.Glob(matcher);
+      if (matcher.includes("/")) {
+        compiled.push(makeGlobMatcher(glob));
+      } else {
+        // matchBase: patterns without slashes match against the basename
+        compiled.push(makeGlobOrBasenameMatcher(glob));
+      }
+    } else if (matcher instanceof RegExp) {
+      compiled.push(makeRegexMatcher(matcher));
+    } else if (typeof matcher === "function") {
+      compiled.push(matcher);
+    } else {
+      throw $ERR_INVALID_ARG_TYPE("options.ignore", ["string", "RegExp", "Function"], matcher);
+    }
+  }
+
+  return function isIgnored(filename) {
+    // With encoding: "buffer" the watcher delivers Buffer filenames; the
+    // string/glob matchers (and basename()) need a string.
+    if (typeof filename !== "string") filename = String(filename);
+    for (const match of compiled) {
+      if (match(filename)) return true;
+    }
+    return false;
+  };
+}
+
+const kFSWatchStart = Symbol("kFSWatchStart");
+
+// Node-compatible whitebox surface: `watcher._handle` is the FSEvent-like handle that
+// delegates to the real native watcher. Replacing it with a foreign object makes
+// close()/[kFSWatchStart]() fail the same internal assertion as Node.
+let closeNativeWatcher: (watcher: FSWatcher) => void;
+let refNativeWatcher: (watcher: FSWatcher) => void;
+let unrefNativeWatcher: (watcher: FSWatcher) => void;
+
+class FSEvent {
+  #owner;
+  constructor(owner) {
+    this.#owner = owner;
+  }
+  close() {
+    closeNativeWatcher(this.#owner);
+  }
+  ref() {
+    refNativeWatcher(this.#owner);
+  }
+  unref() {
+    unrefNativeWatcher(this.#owner);
+  }
+}
+
+function assertFSEventHandle(handle) {
+  if (!(handle instanceof FSEvent)) {
+    throw $ERR_INTERNAL_ASSERTION(
+      "handle must be a FSEvent\n" +
+        "This is caused by either a bug in Node.js or incorrect usage of Node.js internals.\n" +
+        "Please open an issue with this stack trace at https://github.com/nodejs/node/issues\n",
+    );
+  }
+}
+
 class FSWatcher extends EventEmitter {
   #watcher;
   #listener;
+  #ignoreMatcher;
+  _handle;
   constructor(path, options, listener) {
     super();
 
@@ -27,6 +122,7 @@ class FSWatcher extends EventEmitter {
       listener = () => {};
     }
 
+    this.#ignoreMatcher = createIgnoreMatcher(options?.ignore);
     this.#listener = listener;
     try {
       this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this));
@@ -35,6 +131,7 @@ class FSWatcher extends EventEmitter {
       e.filename = path;
       throw e;
     }
+    this._handle = new FSEvent(this);
   }
 
   #onEvent(eventType, filenameOrError) {
@@ -53,30 +150,56 @@ class FSWatcher extends EventEmitter {
 
       this.emit(eventType, filenameOrError);
     } else {
+      if (filenameOrError != null && this.#ignoreMatcher?.(filenameOrError)) {
+        return;
+      }
       this.emit("change", eventType, filenameOrError);
       this.#listener(eventType, filenameOrError);
     }
   }
 
   close() {
-    this.#watcher?.close();
-    this.#watcher = null;
+    assertFSEventHandle(this._handle);
+    this._handle.close();
   }
 
   ref() {
-    this.#watcher?.ref();
+    // like node, honour a replaced _handle and support chaining
+    if (this._handle) this._handle.ref();
+    return this;
   }
 
   unref() {
-    this.#watcher?.unref();
+    if (this._handle) this._handle.unref();
+    return this;
   }
 
   // https://github.com/nodejs/node/blob/9f51c55a47702dc6a0ca3569853dd7ba022bf7bb/lib/internal/fs/watchers.js#L259-L263
   start() {}
+
+  [kFSWatchStart]() {
+    assertFSEventHandle(this._handle);
+  }
+
+  static {
+    // Named function expressions inside the class's static block so they
+    // can read the private #watcher field; assigned to module-level lets so
+    // callers outside the class can invoke them.
+    closeNativeWatcher = function closeNativeWatcher(watcher) {
+      watcher.#watcher?.close();
+      watcher.#watcher = null;
+    };
+    refNativeWatcher = function refNativeWatcher(watcher) {
+      watcher.#watcher?.ref();
+    };
+    unrefNativeWatcher = function unrefNativeWatcher(watcher) {
+      watcher.#watcher?.unref();
+    };
+  }
 }
 
 function watch(path, options, listener) {
   return new FSWatcher(path, options, listener);
 }
 
-export default { watch, FSWatcher };
+export default { watch, FSWatcher, createIgnoreMatcher };
diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts
index f26764e8a503..67db35c2cba1 100644
--- a/src/js/internal/validators.ts
+++ b/src/js/internal/validators.ts
@@ -110,9 +110,31 @@ function throwIfNullBytesInFileName(filename: string) {
   }
 }
 
+/**
+ * node's fs getValidatedPath (lib/internal/fs/utils.js): converts URL
+ * *instances* via fileURLToPath, accepts strings and Buffers as-is (no
+ * path.resolve, no "file:"-prefix string sniffing), and rejects null bytes.
+ */
+function getValidatedFsPath(p: any, propName: string = "path") {
+  if (p instanceof URL) p = Bun.fileURLToPath(p);
+  if (typeof p === "string") {
+    if (p.indexOf("\u0000") !== -1) {
+      throw $ERR_INVALID_ARG_VALUE(propName, p, "must be a string, Uint8Array, or URL without null bytes");
+    }
+    return p;
+  }
+  if (p instanceof Uint8Array) {
+    if (p.indexOf(0) !== -1) {
+      throw $ERR_INVALID_ARG_VALUE(propName, p, "must be a string, Uint8Array, or URL without null bytes");
+    }
+    return p;
+  }
+  throw $ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], p);
+}
+
 hideFromStack(validateLinkHeaderValue, validateInternalField);
 hideFromStack(validateString, validateFunction, validateBoolean, validateUndefined);
-hideFromStack(getValidatedPath, throwIfNullBytesInFileName);
+hideFromStack(getValidatedPath, getValidatedFsPath, throwIfNullBytesInFileName);
 
 export default {
   /** (value, name) */
@@ -160,6 +182,7 @@ export default {
   validateInternalField,
   /** `(path)` — accepts a string or file URL, returns it resolved to an absolute path string */
   getValidatedPath,
+  getValidatedFsPath,
   /** `(filename)` */
   throwIfNullBytesInFileName,
 };
diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts
index ad8ba927c95c..95c4d13c6518 100644
--- a/src/js/node/fs.promises.ts
+++ b/src/js/node/fs.promises.ts
@@ -3,7 +3,7 @@ const types = require("node:util/types");
 const EventEmitter = require("node:events");
 const fs = require("internal/fs/binding") as $ZigGeneratedClasses.NodeJSFS;
 const { glob } = require("internal/fs/glob");
-const { validateInteger } = require("internal/validators");
+const { validateInteger, validateBoolean, validateObject, validateAbortSignal } = require("internal/validators");
 
 const constants = $processBindingConstants.fs;
 
@@ -23,12 +23,26 @@ const kTransferList = Symbol("kTransferList");
 const kDeserialize = Symbol("kDeserialize");
 const kEmptyObject = ObjectFreeze(Object.create(null));
 const kFlag = Symbol("kFlag");
+const kLocked = Symbol("kLocked");
+const kCloseSync = Symbol("kCloseSync");
+
+var SymbolDispose = Symbol.dispose;
+
+// Default chunk size for FileHandle.pull/pullSync/writer (matches Node.js).
+const kIterDefaultChunkSize = 131072;
+
+let nodeFsForIter; // lazy value for require("node:fs") (sync read/write/close for pull/writer).
 
 let Interface; // lazy value for require("node:readline").Interface.
 
 function watch(
   filename: string | Buffer | URL,
-  options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {},
+  options: {
+    encoding?: BufferEncoding;
+    persistent?: boolean;
+    recursive?: boolean;
+    signal?: AbortSignal;
+  } = {},
 ) {
   type Event = {
     eventType: string;
@@ -47,8 +61,40 @@ function watch(
     options = { encoding: options };
   }
   const queue = $createFIFO();
+  const ignoreMatcher = require("internal/fs/watch").createIgnoreMatcher(options?.ignore);
+  const signal = options?.signal;
+  validateAbortSignal(signal, "options.signal");
+  function makeAbortError() {
+    return $makeAbortError(undefined, { cause: signal!.reason });
+  }
+
+  // node never creates the native handle when the signal is already
+  // aborted (its async generator throws on first next() before opening
+  // it); creating one here would leak it, since the "abort" event never
+  // fires for a pre-aborted signal.
+  if (signal?.aborted) {
+    return {
+      [Symbol.asyncIterator]() {
+        let closed = false;
+        return {
+          async next() {
+            if (closed) return { value: undefined, done: true };
+            closed = true;
+            throw makeAbortError();
+          },
+          return() {
+            closed = true;
+            return { value: undefined, done: true };
+          },
+        };
+      },
+    };
+  }
 
   const watcher = fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => {
+    if (eventType !== "close" && eventType !== "error" && filename != null && ignoreMatcher?.(filename)) {
+      return;
+    }
     queue.push({ eventType, filename });
     if (nextEventResolve) {
       const resolve = nextEventResolve;
@@ -57,20 +103,41 @@ function watch(
     }
   });
 
+  function onAbort() {
+    watcher.close();
+    if (nextEventResolve) {
+      const resolve = nextEventResolve;
+      nextEventResolve = null;
+      resolve();
+    }
+  }
+  signal?.addEventListener("abort", onAbort, { once: true });
+  // {once: true} only auto-removes when the event fires; detach explicitly on
+  // the other exit paths so a long-lived signal doesn't retain this closure.
+  function removeAbortListener() {
+    signal?.removeEventListener("abort", onAbort);
+  }
+
   return {
     [Symbol.asyncIterator]() {
       let closed = false;
       return {
         async next() {
           while (!closed) {
+            if (signal?.aborted) {
+              closed = true;
+              throw makeAbortError();
+            }
             let event: Event;
             while ((event = queue.shift() as Event)) {
               if (event.eventType === "close") {
                 closed = true;
+                removeAbortListener();
                 return { value: undefined, done: true };
               }
               if (event.eventType === "error") {
                 closed = true;
+                removeAbortListener();
                 throw event.filename;
               }
               return { value: event, done: false };
@@ -86,6 +153,7 @@ function watch(
           if (!closed) {
             watcher.close();
             closed = true;
+            removeAbortListener();
             if (nextEventResolve) {
               const resolve = nextEventResolve;
               nextEventResolve = null;
@@ -102,25 +170,50 @@ function watch(
 // attempt to use the native code version if possible
 // and on MacOS, simple cases of recursive directory trees can be done in a single `clonefile()`
 // using filter and other options uses a lazily loaded js fallback ported from node.js
-function cp(src, dest, options) {
-  if (!options) return fs.cp(src, dest);
-  if (typeof options !== "object") {
-    throw new TypeError("options must be an object");
-  }
-  if (options.dereference || options.filter || options.preserveTimestamps || options.verbatimSymlinks) {
-    return require("internal/fs/cp")(src, dest, options);
+async function cp(src, dest, options) {
+  const { validateCpOptions } = require("internal/fs/cp-sync");
+  const { getValidatedFsPath } = require("internal/validators");
+  options = validateCpOptions(options);
+  src = getValidatedFsPath(src, "src");
+  dest = getValidatedFsPath(dest, "dest");
+  if (
+    !options.filter &&
+    !options.dereference &&
+    !options.preserveTimestamps &&
+    !options.verbatimSymlinks &&
+    !options.mode &&
+    !options.errorOnExist &&
+    options.force
+  ) {
+    const { ok, checked } = await require("internal/fs/cp").tryNativeFastPath(src, dest, options);
+    if (ok) {
+      return fs.cp(src, dest, options.recursive, options.errorOnExist, options.force, options.mode);
+    }
+    return require("internal/fs/cp").cpFn(src, dest, options, checked);
   }
-  return fs.cp(src, dest, options.recursive, options.errorOnExist, options.force ?? true, options.mode);
+  return require("internal/fs/cp").cpFn(src, dest, options);
+}
+
+function settleFromNodeCallback(resolve, reject, err, value) {
+  if (err) reject(err);
+  else resolve(value);
 }
 
 async function opendir(dir: string, options) {
-  return new (require("node:fs").Dir)(1, dir, options);
+  // Delegate to the callback form so the eager path check (ENOTDIR/ENOENT at
+  // open time, like node) runs on an async stat instead of blocking.
+  const { promise, resolve, reject } = Promise.withResolvers();
+  require("node:fs").opendir(dir, options, settleFromNodeCallback.bind(null, resolve, reject));
+  return promise;
 }
 
 const private_symbols = {
   kRef,
   kUnref,
   kFd,
+  kTransfer,
+  kTransferList,
+  kDeserialize,
   FileHandle: null as any,
 };
 
@@ -160,6 +253,16 @@ const exports = {
   lstat: asyncWrap(fs.lstat, "lstat"),
   mkdir: asyncWrap(fs.mkdir, "mkdir"),
   mkdtemp: asyncWrap(fs.mkdtemp, "mkdtemp"),
+  mkdtempDisposable: async function mkdtempDisposable(prefix, options) {
+    const path = await fs.mkdtemp(prefix, options);
+    // Stash the full path in case of process.chdir()
+    const fullPath = require("node:path").resolve(path);
+    async function remove() {
+      // force makes repeated removal a no-op; real failures (EACCES) still throw
+      await fs.rm(fullPath, { recursive: true, force: true });
+    }
+    return { path, remove, [Symbol.asyncDispose]: remove };
+  },
   statfs: asyncWrap(fs.statfs, "statfs"),
   open: async (path, flags = "r", mode = 0o666) => {
     return new private_symbols.FileHandle(await fs.open(path, flags, mode), flags);
@@ -194,8 +297,35 @@ const exports = {
   unlink: asyncWrap(fs.unlink, "unlink"),
   utimes: asyncWrap(fs.utimes, "utimes"),
   lutimes: asyncWrap(fs.lutimes, "lutimes"),
-  rm: asyncWrap(fs.rm, "rm"),
-  rmdir: asyncWrap(fs.rmdir, "rmdir"),
+  rm: async function rm(path, options) {
+    if (!options?.recursive) {
+      // node validates in JS and reports ERR_FS_EISDIR for directories
+      // (same check as rmSync)
+      let stats;
+      try {
+        stats = await fs.lstat(path);
+      } catch {
+        // let the native call produce the error (respects force/ENOENT)
+      }
+      if (stats?.isDirectory()) {
+        throw require("internal/fs/cp-sync").fsEisdirError({
+          code: "EISDIR",
+          message: "is a directory",
+          path,
+          syscall: "rm",
+          errno: $processBindingConstants.os.errno.EISDIR,
+        });
+      }
+    }
+    return fs.rm(path, options);
+  },
+  rmdir: async function rmdir(path, options) {
+    // node throws for any defined `recursive`, not just truthy ones
+    if (options?.recursive !== undefined) {
+      throw $ERR_INVALID_ARG_VALUE("options.recursive", options.recursive, "is no longer supported");
+    }
+    return fs.rmdir(path, options);
+  },
   writev: async (fd, buffers, position) => {
     var bytesWritten = await fs.writev(fd, buffers, position);
     return {
@@ -478,7 +608,10 @@ function asyncWrap(fn: any, name: string) {
       }
       try {
         this[kRef]();
-        return { buffer, bytesWritten: await write(fd, buffer, offset, length, position) };
+        return {
+          buffer,
+          bytesWritten: await write(fd, buffer, offset, length, position),
+        };
       } finally {
         this[kUnref]();
       }
@@ -512,7 +645,11 @@ function asyncWrap(fn: any, name: string) {
 
       try {
         this[kRef]();
-        return await writeFile(fd, data, { encoding, flag: this[kFlag], signal });
+        return await writeFile(fd, data, {
+          encoding,
+          flag: this[kFlag],
+          signal,
+        });
       } finally {
         this[kUnref]();
       }
@@ -582,16 +719,661 @@ function asyncWrap(fn: any, name: string) {
       });
     }
 
+    // Port of Node.js FileHandle.prototype.pull (lib/internal/fs/promises.js).
+    // Returns the file contents as an AsyncIterable using the
+    // iterable streams pull model. Optional transforms and options (including
+    // AbortSignal) may be provided as trailing arguments.
+    pull(...args) {
+      if (this[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+      if (this[kClosePromise]) throw $ERR_INVALID_STATE("The FileHandle is closing");
+      if (this[kLocked]) throw $ERR_INVALID_STATE("The FileHandle is locked");
+
+      const { parsePullArgs } = require("internal/streams/iter/utils");
+      const { transforms, options = kEmptyObject } = parsePullArgs(args);
+
+      const { autoClose = false, chunkSize: readSize = kIterDefaultChunkSize, signal } = options;
+      let { start: pos = -1, limit: remaining = -1 } = options;
+
+      const handle = this;
+      const fd = this[kFd];
+
+      validateBoolean(autoClose, "options.autoClose");
+
+      if (pos !== -1) {
+        validateInteger(pos, "options.start", 0);
+      }
+      if (remaining !== -1) {
+        validateInteger(remaining, "options.limit", 1);
+      }
+      if (readSize !== undefined) {
+        validateInteger(readSize, "options.chunkSize", 1);
+      }
+      if (signal !== undefined) {
+        validateAbortSignal(signal, "options.signal");
+      }
+
+      if (signal?.aborted) {
+        // Don't lock the handle: with transforms, the pull pipeline's
+        // pre-abort branch returns a rejecting iterator without ever
+        // consuming the source, so the unlock in its finally would never
+        // run. Reject on first next() like the source itself would.
+        return {
+          __proto__: null,
+          [Symbol.asyncIterator]() {
+            let done = false;
+            return {
+              __proto__: null,
+              async next() {
+                if (done) return { value: undefined, done: true };
+                done = true;
+                if (autoClose) await handle.close();
+                throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
+              },
+              async return() {
+                if (!done) {
+                  done = true;
+                  if (autoClose) await handle.close();
+                }
+                return { value: undefined, done: true };
+              },
+            };
+          },
+        };
+      }
+
+      this[kLocked] = true;
+
+      const source = {
+        __proto__: null,
+        async *[Symbol.asyncIterator]() {
+          // The fd was captured when pull() was called; the handle may have
+          // been closed in between (an unstarted source doesn't hold a ref).
+          if (handle[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+          handle[kRef]();
+          try {
+            while (remaining !== 0) {
+              if (signal?.aborted) {
+                throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
+              }
+              const toRead = remaining > 0 ? Math.min(readSize, remaining) : readSize;
+              const buf = Buffer.allocUnsafe(toRead);
+              const bytesRead = (await read(fd, buf, 0, toRead, pos >= 0 ? pos : null)) || 0;
+              if (bytesRead === 0) break;
+              if (pos >= 0) pos += bytesRead;
+              if (remaining > 0) remaining -= bytesRead;
+              yield [bytesRead < toRead ? buf.subarray(0, bytesRead) : buf];
+            }
+          } finally {
+            handle[kLocked] = false;
+            handle[kUnref]();
+            if (autoClose) {
+              await handle.close();
+            }
+          }
+        },
+      };
+
+      // If transforms provided, wrap with pull pipeline
+      if (transforms.length > 0) {
+        const pullArgs = [...transforms];
+        if (options) {
+          pullArgs.push(options);
+        }
+        return require("internal/streams/iter/pull").pull(source, ...pullArgs);
+      }
+      return source;
+    }
+
+    // Port of Node.js FileHandle.prototype.pullSync. Returns the file
+    // contents as an Iterable using synchronous reads.
+    pullSync(...args) {
+      if (this[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+      if (this[kClosePromise]) throw $ERR_INVALID_STATE("The FileHandle is closing");
+      if (this[kLocked]) throw $ERR_INVALID_STATE("The FileHandle is locked");
+
+      const { parsePullArgs } = require("internal/streams/iter/utils");
+      const { transforms, options = kEmptyObject } = parsePullArgs(args);
+
+      const { autoClose = false, chunkSize: readSize = kIterDefaultChunkSize } = options;
+      let { start: pos = -1, limit: remaining = -1 } = options;
+
+      const handle = this;
+      const fd = this[kFd];
+
+      validateBoolean(autoClose, "options.autoClose");
+
+      if (pos !== -1) {
+        validateInteger(pos, "options.start", 0);
+      }
+      if (remaining !== -1) {
+        validateInteger(remaining, "options.limit", 1);
+      }
+      if (readSize !== undefined) {
+        validateInteger(readSize, "options.chunkSize", 1);
+      }
+
+      this[kLocked] = true;
+
+      const fsSync = (nodeFsForIter ??= require("node:fs"));
+
+      const source = {
+        __proto__: null,
+        [Symbol.iterator]() {
+          // The fd was captured when pullSync() was called; the handle may
+          // have been closed in between (an unstarted source doesn't hold a
+          // ref).
+          if (handle[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+          // Acquire the ref per iteration (like pull()'s async generator), so
+          // an iterable that is never consumed doesn't pin the handle open;
+          // cleanup is idempotent so a stray next() after return() can't
+          // double-unref.
+          handle[kRef]();
+          let done = false;
+          let cleanedUp = false;
+          function cleanup() {
+            if (cleanedUp) return;
+            cleanedUp = true;
+            handle[kLocked] = false;
+            handle[kUnref]();
+            if (autoClose) {
+              handle[kCloseSync]();
+            }
+          }
+          return {
+            __proto__: null,
+            next() {
+              if (done || remaining === 0) {
+                if (!done) {
+                  done = true;
+                  cleanup();
+                }
+                return { value: undefined, done: true };
+              }
+              const toRead = remaining > 0 ? Math.min(readSize, remaining) : readSize;
+              const buf = Buffer.allocUnsafe(toRead);
+              let bytesRead;
+              try {
+                bytesRead = fsSync.readSync(fd, buf, 0, toRead, pos >= 0 ? pos : null) || 0;
+              } catch (err) {
+                done = true;
+                cleanup();
+                throw err;
+              }
+              if (bytesRead === 0) {
+                done = true;
+                cleanup();
+                return { value: undefined, done: true };
+              }
+              if (pos >= 0) pos += bytesRead;
+              if (remaining > 0) remaining -= bytesRead;
+              const chunk = bytesRead < toRead ? buf.subarray(0, bytesRead) : buf;
+              return { value: [chunk], done: false };
+            },
+            return() {
+              if (!done) {
+                done = true;
+                cleanup();
+              }
+              return { value: undefined, done: true };
+            },
+          };
+        },
+      };
+
+      if (transforms.length > 0) {
+        return require("internal/streams/iter/pull").pullSync(source, ...transforms);
+      }
+      return source;
+    }
+
+    // Port of Node.js FileHandle.prototype.writer. Returns an iterable-streams
+    // Writer backed by this file handle. Supports writev() for batch writes,
+    // handles zero-byte writes with retry (up to 5 attempts).
+    writer(options = kEmptyObject) {
+      if (this[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+      if (this[kClosePromise]) throw $ERR_INVALID_STATE("The FileHandle is closing");
+      if (this[kLocked]) throw $ERR_INVALID_STATE("The FileHandle is locked");
+
+      const { toUint8Array, convertChunks } = require("internal/streams/iter/utils");
+
+      validateObject(options, "options");
+      const { autoClose = false, chunkSize: syncWriteThreshold = kIterDefaultChunkSize } = options;
+      let { start: pos = -1, limit: bytesRemaining = -1 } = options;
+
+      const handle = this;
+      const fd = this[kFd];
+      let totalBytesWritten = 0;
+      let closed = false;
+      let closing = false;
+      let pendingEndPromise = null;
+      let error = null;
+      // Count of in-flight async writes (write() doesn't serialize callers,
+      // so several can be on the threadpool at once).
+      let asyncPending = 0;
+      // Set when end()/fail() must tear down while an async write is still on
+      // the threadpool: writeAll/writevAll run it from their finally so the
+      // fd is never closed under an in-flight write.
+      let deferredTeardown: (() => void) | null = null;
+      function runDeferredTeardown() {
+        if (deferredTeardown !== null) {
+          const teardown = deferredTeardown;
+          deferredTeardown = null;
+          teardown();
+        }
+      }
+
+      validateBoolean(autoClose, "options.autoClose");
+
+      if (pos !== -1) {
+        validateInteger(pos, "options.start", 0);
+      }
+      if (bytesRemaining !== -1) {
+        validateInteger(bytesRemaining, "options.limit", 1);
+      }
+      if (syncWriteThreshold !== undefined) {
+        validateInteger(syncWriteThreshold, "options.chunkSize", 1);
+      }
+
+      this[kLocked] = true;
+      // Acquire the ref on first actual write (like pull/pullSync defer it to
+      // iteration) so an unused writer can't pin the handle and hang close().
+      let refAcquired = false;
+      function acquireRef() {
+        if (!refAcquired) {
+          refAcquired = true;
+          handle[kRef]();
+        }
+      }
+      function releaseRef() {
+        if (refAcquired) {
+          refAcquired = false;
+          handle[kUnref]();
+        }
+      }
+
+      const fsSync = (nodeFsForIter ??= require("node:fs"));
+
+      // Write a single buffer with retry on zero-byte writes (up to 5 retries).
+      async function writeAll(buf, offset, length, position, signal) {
+        asyncPending++;
+        try {
+          let retries = 0;
+          while (length > 0) {
+            const bytesWritten = (await write(fd, buf, offset, length, position >= 0 ? position : null)) || 0;
+
+            signal?.throwIfAborted();
+
+            if (bytesWritten === 0) {
+              if (++retries > 5) {
+                throw $ERR_OPERATION_FAILED("Operation failed: write failed after retries");
+              }
+            } else {
+              retries = 0;
+            }
+
+            totalBytesWritten += bytesWritten;
+            offset += bytesWritten;
+            length -= bytesWritten;
+            if (position >= 0) position += bytesWritten;
+          }
+        } catch (err) {
+          // A failed/aborted write may have hit the disk partially and the
+          // cursor/limit were advanced optimistically; the writer's state is
+          // no longer trustworthy, so poison it like fail() does.
+          if (!closed && !error) error = err;
+          throw err;
+        } finally {
+          if (--asyncPending === 0) {
+            runDeferredTeardown();
+          }
+        }
+      }
+
+      // Writev with retry. On partial write, concatenates remaining
+      // buffers and falls back to writeAll.
+      async function writevAll(buffers, position, signal) {
+        asyncPending++;
+        try {
+          let totalSize = 0;
+          for (let i = 0; i < buffers.length; i++) {
+            totalSize += buffers[i].byteLength;
+          }
+
+          let retries = 0;
+          while (totalSize > 0) {
+            const { bytesWritten } = await writev(fd, buffers, position >= 0 ? position : null);
+
+            signal?.throwIfAborted();
+
+            if (bytesWritten === 0) {
+              // Retry the writev as-is on a zero-byte write (up to 5 times)
+              // instead of degrading to the concat fallback below.
+              if (++retries > 5) {
+                throw $ERR_OPERATION_FAILED("Operation failed: writev failed after retries");
+              }
+              continue;
+            }
+            retries = 0;
+
+            totalBytesWritten += bytesWritten;
+            totalSize -= bytesWritten;
+            if (position >= 0) position += bytesWritten;
+
+            if (totalSize > 0) {
+              // Partial write - concatenate remaining and use writeAll.
+              const remaining = Buffer.concat(buffers);
+              const wrote = bytesWritten;
+              await writeAll(remaining, wrote, remaining.length - wrote, position, signal);
+              return;
+            }
+          }
+        } catch (err) {
+          // See writeAll: the optimistic cursor/limit accounting is invalid
+          // after a failure, so subsequent writes must reject.
+          if (!closed && !error) error = err;
+          throw err;
+        } finally {
+          if (--asyncPending === 0) {
+            runDeferredTeardown();
+          }
+        }
+      }
+
+      // Synchronous write with retry. Throws on I/O error.
+      function writeSyncAll(buf, offset, length, position) {
+        let retries = 0;
+        while (length > 0) {
+          const bytesWritten = fsSync.writeSync(fd, buf, offset, length, position >= 0 ? position : null) || 0;
+          if (bytesWritten === 0) {
+            if (++retries > 5) {
+              throw $ERR_OPERATION_FAILED("Operation failed: write failed after retries");
+            }
+          } else {
+            retries = 0;
+          }
+          totalBytesWritten += bytesWritten;
+          offset += bytesWritten;
+          length -= bytesWritten;
+          if (position >= 0) position += bytesWritten;
+        }
+      }
+
+      function returnTotalBytesWritten() {
+        return totalBytesWritten;
+      }
+
+      async function cleanup() {
+        if (closed) return;
+        closed = true;
+        handle[kLocked] = false;
+        if (asyncPending) {
+          const { promise, resolve, reject } = Promise.withResolvers();
+          deferredTeardown = function deferredCleanupTeardown() {
+            releaseRef();
+            if (autoClose) {
+              handle.close().$then(resolve, reject);
+            } else {
+              resolve(undefined);
+            }
+          };
+          return promise;
+        }
+        releaseRef();
+        if (autoClose) {
+          await handle.close();
+        }
+      }
+
+      return {
+        __proto__: null,
+        write(chunk, options = kEmptyObject) {
+          if (error) {
+            return Promise.$reject(error);
+          }
+          if (closed) {
+            return Promise.$reject($ERR_INVALID_STATE_TypeError("The writer is closed"));
+          }
+          if (handle[kFd] === -1) {
+            // The handle was closed before this writer took its ref.
+            return Promise.$reject($ERR_INVALID_STATE("The FileHandle is closed"));
+          }
+          validateObject(options, "options");
+          const { signal } = options;
+          if (signal !== undefined) {
+            validateAbortSignal(signal, "options.signal");
+            if (signal.aborted) {
+              return Promise.$reject(signal.reason);
+            }
+          }
+          chunk = toUint8Array(chunk);
+          if (bytesRemaining >= 0 && chunk.byteLength > bytesRemaining) {
+            return Promise.$reject($ERR_OUT_OF_RANGE("write", `<= ${bytesRemaining} bytes`, chunk.byteLength));
+          }
+          if (bytesRemaining > 0) bytesRemaining -= chunk.byteLength;
+          const position = pos;
+          if (pos >= 0) pos += chunk.byteLength;
+          acquireRef();
+          return writeAll(chunk, 0, chunk.byteLength, position, signal);
+        },
+
+        writev(chunks, options = kEmptyObject) {
+          if (error) {
+            return Promise.$reject(error);
+          }
+          if (closed) {
+            return Promise.$reject($ERR_INVALID_STATE_TypeError("The writer is closed"));
+          }
+          if (handle[kFd] === -1) {
+            return Promise.$reject($ERR_INVALID_STATE("The FileHandle is closed"));
+          }
+          validateObject(options, "options");
+          const { signal } = options;
+          if (signal !== undefined) {
+            validateAbortSignal(signal, "options.signal");
+            if (signal?.aborted) {
+              return Promise.$reject(signal.reason);
+            }
+          }
+          chunks = convertChunks(chunks);
+          let totalSize = 0;
+          for (let i = 0; i < chunks.length; i++) {
+            totalSize += chunks[i].byteLength;
+          }
+          if (bytesRemaining >= 0 && totalSize > bytesRemaining) {
+            return Promise.$reject($ERR_OUT_OF_RANGE("writev", `<= ${bytesRemaining} bytes`, totalSize));
+          }
+          if (bytesRemaining > 0) bytesRemaining -= totalSize;
+          const position = pos;
+          if (pos >= 0) pos += totalSize;
+          acquireRef();
+          return writevAll(chunks, position, signal);
+        },
+
+        writeSync(chunk) {
+          if (error || closed || asyncPending) return false;
+          if (handle[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+          chunk = toUint8Array(chunk);
+          const length = chunk.byteLength;
+          if (length > syncWriteThreshold) return false;
+          if (length === 0) return true;
+          if (bytesRemaining >= 0 && length > bytesRemaining) return false;
+          const position = pos;
+          // First attempt - if this fails, return false so pipeTo can
+          // fall back to async write().
+          let bytesWritten;
+          acquireRef();
+          try {
+            bytesWritten = fsSync.writeSync(fd, chunk, 0, length, position >= 0 ? position : null) || 0;
+          } catch {
+            return false;
+          }
+          totalBytesWritten += bytesWritten;
+          if (position >= 0) {
+            pos = position + bytesWritten;
+          }
+          if (bytesWritten === length) {
+            if (bytesRemaining > 0) bytesRemaining -= length;
+            return true;
+          }
+          // Partial write - bytes are on disk. Must complete or throw.
+          writeSyncAll(chunk, bytesWritten, length - bytesWritten, position >= 0 ? position + bytesWritten : -1);
+          // writeSyncAll only advances its local position; move the cursor
+          // past the whole chunk so the next write doesn't overwrite its tail.
+          if (position >= 0) {
+            pos = position + length;
+          }
+          if (bytesRemaining > 0) bytesRemaining -= length;
+          return true;
+        },
+
+        writevSync(chunks) {
+          if (error || closed || asyncPending) return false;
+          if (handle[kFd] === -1) throw $ERR_INVALID_STATE("The FileHandle is closed");
+          chunks = convertChunks(chunks);
+          let totalSize = 0;
+          for (let i = 0; i < chunks.length; i++) {
+            totalSize += chunks[i].byteLength;
+          }
+          if (totalSize > syncWriteThreshold) return false;
+          if (totalSize === 0) return true;
+          if (bytesRemaining >= 0 && totalSize > bytesRemaining) return false;
+          const position = pos;
+          let bytesWritten;
+          acquireRef();
+          try {
+            bytesWritten = fsSync.writevSync(fd, chunks, position >= 0 ? position : null) || 0;
+          } catch {
+            return false;
+          }
+          totalBytesWritten += bytesWritten;
+          if (position >= 0) {
+            pos = position + bytesWritten;
+          }
+          if (bytesWritten === totalSize) {
+            if (bytesRemaining > 0) bytesRemaining -= totalSize;
+            return true;
+          }
+          // Partial writev - bytes are on disk. Must complete or throw.
+          const rest = Buffer.concat(chunks);
+          writeSyncAll(
+            rest,
+            bytesWritten,
+            rest.byteLength - bytesWritten,
+            position >= 0 ? position + bytesWritten : -1,
+          );
+          // writeSyncAll only advances its local position; move the cursor
+          // past all chunks so the next write doesn't overwrite their tail.
+          if (position >= 0) {
+            pos = position + totalSize;
+          }
+          if (bytesRemaining > 0) bytesRemaining -= totalSize;
+          return true;
+        },
+
+        end(options = kEmptyObject) {
+          if (error) {
+            return Promise.$reject(error);
+          }
+          if (closed) {
+            return Promise.$resolve(totalBytesWritten);
+          }
+          if (closing) {
+            return pendingEndPromise;
+          }
+          validateObject(options, "options");
+          const { signal } = options;
+          if (signal !== undefined) {
+            validateAbortSignal(signal, "options.signal");
+            if (signal.aborted) {
+              return Promise.$reject(signal.reason);
+            }
+          }
+          closing = true;
+          pendingEndPromise = cleanup().$then(returnTotalBytesWritten);
+          return pendingEndPromise;
+        },
+
+        endSync() {
+          if (error) return -1;
+          if (closed) return totalBytesWritten;
+          if (asyncPending) return -1;
+          closed = true;
+          handle[kLocked] = false;
+          releaseRef();
+          if (autoClose) {
+            handle[kCloseSync]();
+          }
+          return totalBytesWritten;
+        },
+
+        fail(reason) {
+          if (closed || error) return;
+          error = reason ?? $ERR_INVALID_STATE("Failed");
+          closed = true;
+          handle[kLocked] = false;
+          function teardown() {
+            releaseRef();
+            if (autoClose) {
+              handle[kCloseSync]();
+            }
+          }
+          if (asyncPending) {
+            // an async write is still using the fd - tear down after it lands
+            deferredTeardown = teardown;
+            return;
+          }
+          teardown();
+        },
+
+        [SymbolAsyncDispose]() {
+          if (closing) {
+            return pendingEndPromise ?? Promise.$resolve();
+          }
+          if (!closed && !error) {
+            this.fail();
+          }
+          return Promise.$resolve();
+        },
+
+        [SymbolDispose]() {
+          this.fail();
+        },
+      };
+    }
+
+    // Synchronously close the FileHandle (used by pullSync/writer autoClose).
+    [kCloseSync]() {
+      if (this[kFd] === -1) return;
+      if (this[kClosePromise]) {
+        throw $ERR_INVALID_STATE("The FileHandle is closing");
+      }
+      const fd = this[kFd];
+      this[kFd] = -1;
+      (nodeFsForIter ??= require("node:fs")).closeSync(fd);
+      this.emit("close");
+    }
+
     [kTransfer]() {
-      throw new Error("BUN TODO FileHandle.kTransfer");
+      if (this[kClosePromise] || this[kRefs] > 1) {
+        throw new DOMException("Cannot transfer FileHandle while in use", "DataCloneError");
+      }
+
+      const fd = this[kFd];
+      const flag = this[kFlag];
+      this[kFd] = -1;
+      return {
+        data: { fd, flag },
+        deserializeInfo: "internal/fs/promises:FileHandle",
+      };
     }
 
     [kTransferList]() {
-      throw new Error("BUN TODO FileHandle.kTransferList");
+      return [];
     }
 
-    [kDeserialize](_) {
-      throw new Error("BUN TODO FileHandle.kDeserialize");
+    [kDeserialize]({ fd, flag }) {
+      this[kFd] = fd;
+      this[kFlag] = flag;
     }
 
     [kRef]() {
@@ -600,8 +1382,13 @@ function asyncWrap(fn: any, name: string) {
 
     [kUnref]() {
       if (--this[kRefs] === 0) {
+        // Close the captured fd directly: this.close() would see kFd === -1
+        // and short-circuit without ever closing the descriptor, leaking it
+        // on the deferred-close path (close() called while an op was still
+        // in flight).
+        const fd = this[kFd];
         this[kFd] = -1;
-        this.close().$then(this[kCloseResolve], this[kCloseReject]);
+        (fd !== -1 ? close(fd) : Promise.$resolve()).$then(this[kCloseResolve], this[kCloseReject]);
       }
     }
   }
diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts
index ccd781d40df3..e3d8bfbd06a6 100644
--- a/src/js/node/fs.ts
+++ b/src/js/node/fs.ts
@@ -80,14 +80,20 @@ var access = function access(path, mode, callback) {
     }
 
     ensureCallback(callback);
-    fs.rm(path, options).then(nullcallback(callback), callback);
+    // route through promises.rm for the JS-side ERR_FS_EISDIR validation
+    promises.rm(path, options).then(nullcallback(callback), callback);
   },
   rmdir = function rmdir(path, options, callback) {
     if ($isCallable(options)) {
       callback = options;
       options = undefined;
     }
+    callback = ensureCallback(callback);
 
+    // node throws for any defined `recursive`, not just truthy ones
+    if (options?.recursive !== undefined) {
+      throw $ERR_INVALID_ARG_VALUE("options.recursive", options.recursive, "is no longer supported");
+    }
     fs.rmdir(path, options).then(nullcallback(callback), callback);
   },
   copyFile = function copyFile(src, dest, mode, callback) {
@@ -363,6 +369,12 @@ var access = function access(path, mode, callback) {
 
     ensureCallback(callback);
 
+    const signal = options?.signal;
+    if (signal?.aborted) {
+      process.nextTick(callback, $makeAbortError(undefined, { cause: signal.reason }));
+      return;
+    }
+
     fs.stat(path, options).then(function (stats) {
       callback(null, stats);
     }, callback);
@@ -447,6 +459,16 @@ var access = function access(path, mode, callback) {
   lstatSync = fs.lstatSync.bind(fs) as unknown as typeof import("node:fs").lstatSync,
   mkdirSync = fs.mkdirSync.bind(fs) as unknown as typeof import("node:fs").mkdirSync,
   mkdtempSync = fs.mkdtempSync.bind(fs) as unknown as typeof import("node:fs").mkdtempSync,
+  mkdtempDisposableSync = function mkdtempDisposableSync(prefix, options) {
+    const path = mkdtempSync(prefix, options);
+    // Stash the full path in case of process.chdir()
+    const fullPath = require("node:path").resolve(path);
+    function remove() {
+      // force makes repeated removal a no-op; real failures (EACCES) still throw
+      fs.rmSync(fullPath, { recursive: true, force: true });
+    }
+    return { path, remove, [Symbol.dispose]: remove };
+  },
   openSync = fs.openSync.bind(fs) as unknown as typeof import("node:fs").openSync,
   readSync = function readSync(fd, buffer, offsetOrOptions, length, position) {
     let offset = offsetOrOptions;
@@ -463,7 +485,32 @@ var access = function access(path, mode, callback) {
 
     return fs.readSync(fd, buffer, offset, length, position);
   },
-  writeSync = fs.writeSync.bind(fs),
+  writeSync = function writeSync(fd, buffer, offsetOrOptions, length, position) {
+    try {
+      if (types.isArrayBufferView(buffer)) {
+        let offset = offsetOrOptions;
+        if (typeof offset === "object" && offset !== null) {
+          ({ offset = 0, length = buffer.byteLength - offset, position = null } = offsetOrOptions);
+          return fs.writeSync(fd, buffer, offset, length, position);
+        }
+        return arguments.length <= 2 ? fs.writeSync(fd, buffer) : fs.writeSync(fd, buffer, offset, length, position);
+      }
+      if (typeof buffer !== "string") {
+        throw $ERR_INVALID_ARG_TYPE("buffer", ["string", "Buffer", "TypedArray", "DataView"], buffer);
+      }
+      return fs.writeSync(fd, buffer, offsetOrOptions, length);
+    } catch (err) {
+      // Node's fs binding reports sync write failures by assigning the error
+      // context onto a plain object with ordinary assignment semantics, so
+      // accessors installed on Object.prototype observe (and can replace) the
+      // error instead of crashing the process. Replicate that contract.
+      const ctx = {};
+      ctx.errno = err?.errno;
+      ctx.syscall = err?.syscall;
+      ctx.code = err?.code;
+      throw err;
+    }
+  },
   readdirSync = fs.readdirSync.bind(fs),
   readFileSync = fs.readFileSync.bind(fs),
   fdatasyncSync = fs.fdatasyncSync.bind(fs),
@@ -477,8 +524,34 @@ var access = function access(path, mode, callback) {
   unlinkSync = fs.unlinkSync.bind(fs),
   utimesSync = fs.utimesSync.bind(fs),
   lutimesSync = fs.lutimesSync.bind(fs),
-  rmSync = fs.rmSync.bind(fs),
-  rmdirSync = fs.rmdirSync.bind(fs),
+  rmSync = function rmSync(path, options) {
+    if (!options?.recursive) {
+      // node validates in JS and reports ERR_FS_EISDIR for directories
+      let stats;
+      try {
+        stats = fs.lstatSync(path);
+      } catch {
+        // let the native call produce the error (respects force/ENOENT)
+      }
+      if (stats?.isDirectory()) {
+        throw require("internal/fs/cp-sync").fsEisdirError({
+          code: "EISDIR",
+          message: "is a directory",
+          path,
+          syscall: "rm",
+          errno: $processBindingConstants.os.errno.EISDIR,
+        });
+      }
+    }
+    return fs.rmSync(path, options);
+  },
+  rmdirSync = function rmdirSync(path, options) {
+    // node throws for any defined `recursive`, not just truthy ones
+    if (options?.recursive !== undefined) {
+      throw $ERR_INVALID_ARG_VALUE("options.recursive", options.recursive, "is no longer supported");
+    }
+    return fs.rmdirSync(path, options);
+  },
   writev = function writev(fd, buffers, position, callback) {
     if (typeof position === "function") {
       callback = position;
@@ -514,8 +587,17 @@ var access = function access(path, mode, callback) {
       options = undefined;
     }
     validateFunction(callback, "callback");
-    const result = new Dir(1, path, options);
-    callback(null, result);
+    // Argument validation errors throw synchronously (node does the same);
+    // the eager path check runs on an async stat so the JS thread isn't
+    // blocked and the callback never fires synchronously.
+    const result = new Dir(1, path, options, kAlreadyValidated);
+    // Invoke the callback from process.nextTick so an exception thrown by it
+    // surfaces as an uncaught exception instead of rejecting this internal
+    // promise chain (same convention as glob() below).
+    fs.stat(path).then(
+      onOpendirStatFulfilled.bind(null, callback, path, result),
+      onOpendirStatRejected.bind(null, callback, path),
+    );
   };
 
 const { defineCustomPromisifyArgs } = require("internal/promisify");
@@ -848,14 +930,27 @@ realpathSync.native = fs.realpathNativeSync.bind(fs);
 // and on MacOS, simple cases of recursive directory trees can be done in a single `clonefile()`
 // using filter and other options uses a lazily loaded js fallback ported from node.js
 function cpSync(src, dest, options) {
-  if (!options) return fs.cpSync(src, dest);
-  if (typeof options !== "object") {
-    throw new TypeError("options must be an object");
-  }
-  if (options.dereference || options.filter || options.preserveTimestamps || options.verbatimSymlinks) {
-    return require("internal/fs/cp-sync")(src, dest, options);
+  const { cpSyncFn, validateCpOptions, tryNativeFastPathSync } = require("internal/fs/cp-sync");
+  const { getValidatedFsPath } = require("internal/validators");
+  options = validateCpOptions(options);
+  src = getValidatedFsPath(src, "src");
+  dest = getValidatedFsPath(dest, "dest");
+  if (
+    !options.filter &&
+    !options.dereference &&
+    !options.preserveTimestamps &&
+    !options.verbatimSymlinks &&
+    !options.mode &&
+    !options.errorOnExist &&
+    options.force
+  ) {
+    const { ok, checked } = tryNativeFastPathSync(src, dest, options);
+    if (ok) {
+      return fs.cpSync(src, dest, options.recursive, options.errorOnExist, options.force, options.mode);
+    }
+    return cpSyncFn(src, dest, options, checked);
   }
-  return fs.cpSync(src, dest, options.recursive, options.errorOnExist, options.force ?? true, options.mode);
+  return cpSyncFn(src, dest, options);
 }
 
 function cp(src, dest, options, callback) {
@@ -866,7 +961,14 @@ function cp(src, dest, options, callback) {
 
   ensureCallback(callback);
 
-  promises.cp(src, dest, options).then(() => callback(), callback);
+  // node's callback form throws synchronously on invalid options/paths
+  const { validateCpOptions } = require("internal/fs/cp-sync");
+  const { getValidatedFsPath } = require("internal/validators");
+  options = validateCpOptions(options);
+  src = getValidatedFsPath(src, "src");
+  dest = getValidatedFsPath(dest, "dest");
+
+  promises.cp(src, dest, options).then(callOnceWithNull.bind(null, callback), callback);
 }
 
 function _toUnixTimestamp(time: any, name = "time") {
@@ -888,12 +990,53 @@ function _toUnixTimestamp(time: any, name = "time") {
   throw $ERR_INVALID_ARG_TYPE(name, "number or Date", time);
 }
 
+function onOpendirStatFulfilled(callback, path, result, stats) {
+  if (!stats.isDirectory()) {
+    process.nextTick(callback, opendirNotDirError(path));
+    return;
+  }
+  process.nextTick(callback, null, result);
+}
+function onOpendirStatRejected(callback, path, err) {
+  process.nextTick(callback, typeof err?.errno === "number" ? opendirStatError(err, path) : err);
+}
+function callOnceWithNull(callback) {
+  callback(null);
+}
+function callOnceWithNullThen(callback, value) {
+  callback(null, value);
+}
+
 function opendirSync(path, options) {
   // TODO: validatePath
   // validateString(path, "path");
   return new Dir(1, path, options);
 }
 
+// Reshape a stat error as node's eager opendir error. Stat errors arrive as
+// "ECODE: , stat ''"; pull out just the description before
+// re-prefixing (avoids "EACCES: EACCES: ...").
+function opendirStatError(err, path) {
+  err.syscall = "opendir";
+  const description = err.message.replace(/^[A-Z]+: /, "").replace(/, l?stat '.*'$/, "");
+  err.message = `${err.code}: ${description}, opendir '${path}'`;
+  return err;
+}
+
+function opendirNotDirError(path) {
+  const err = new Error(`ENOTDIR: not a directory, opendir '${path}'`);
+  err.code = "ENOTDIR";
+  // libuv's UV_ENOTDIR: -ENOTDIR on POSIX, -4052 on Windows
+  err.errno = process.platform === "win32" ? -4052 : -20;
+  err.syscall = "opendir";
+  err.path = path;
+  return err;
+}
+
+// Passed as the Dir constructor's 4th argument by the async opendir paths,
+// which run the eager path check with an async stat instead.
+const kAlreadyValidated = Symbol("kAlreadyValidated");
+
 class Dir {
   /**
    * `-1` when closed. stdio handles (0, 1, 2) don't actually get closed by
@@ -903,77 +1046,159 @@ class Dir {
   #path: PathLike;
   #options;
   #entries: DirentType[] | null = null;
+  #entriesIdx = 0;
 
-  constructor(handle, path: PathLike, options) {
+  constructor(handle, path: PathLike, options, validated?) {
     if ($isUndefinedOrNull(handle)) throw $ERR_MISSING_ARGS("handle");
     validateInteger(handle, "handle", 0);
+    if (options != null && typeof options !== "object" && typeof options !== "string") {
+      throw $ERR_INVALID_ARG_TYPE("options", "object", options);
+    }
+    // node's getOptions: a string is encoding shorthand
+    if (typeof options === "string") options = { encoding: options };
+    const encoding = options?.encoding;
+    if (encoding != null && encoding !== "buffer" && !Buffer.isEncoding(encoding)) {
+      throw $ERR_INVALID_ARG_VALUE("encoding", encoding, "is invalid encoding");
+    }
+    if (options?.bufferSize !== undefined) {
+      validateInteger(options.bufferSize, "options.bufferSize", 1);
+    }
+    if (handle === 1 && validated !== kAlreadyValidated) {
+      // node's opendir opens the directory eagerly and reports ENOTDIR/ENOENT
+      let stats;
+      try {
+        stats = fs.statSync(path);
+      } catch (err: any) {
+        if (typeof err?.errno !== "number") throw err; // argument validation errors throw as-is
+        throw opendirStatError(err, path);
+      }
+      if (!stats.isDirectory()) {
+        throw opendirNotDirError(path);
+      }
+    }
     this.#handle = $toLength(handle);
     this.#path = path;
     this.#options = options;
   }
 
+  // Number of in-flight async operations; sync ops are forbidden while > 0,
+  // and async ops queue behind #pendingOp like node's operation queue.
+  #pendingCount = 0;
+  #pendingOp: Promise | null = null;
+
+  #dirConcurrentError() {
+    return $ERR_DIR_CONCURRENT_OPERATION(
+      "Cannot do synchronous work on directory handle with concurrent asynchronous operations",
+    );
+  }
+
+  #enqueue(run) {
+    const prev = this.#pendingOp;
+    let p;
+    if (prev) {
+      p = prev.then(run, run);
+    } else {
+      try {
+        const r = run();
+        p = $isPromise(r) ? r : Promise.$resolve(r);
+      } catch (e) {
+        p = Promise.$reject(e);
+      }
+    }
+    this.#pendingCount++;
+    this.#pendingOp = p;
+    const done = this.#opDone.bind(this);
+    p.then(done, done);
+    return p;
+  }
+
   readSync() {
     if (this.#handle < 0) throw $ERR_DIR_CLOSED();
+    if (this.#pendingCount > 0) throw this.#dirConcurrentError();
 
     let entries = (this.#entries ??= fs.readdirSync(this.#path, {
       withFileTypes: true,
       encoding: this.#options?.encoding,
       recursive: this.#options?.recursive,
     }));
-    return entries.shift() ?? null;
+    return this.#entriesIdx < entries.length ? entries[this.#entriesIdx++] : null;
   }
 
   read(cb?: (err: Error | null, entry: DirentType) => void): any {
-    if (this.#handle < 0) throw $ERR_DIR_CLOSED();
-
     if (!$isUndefinedOrNull(cb)) {
       validateFunction(cb, "callback");
-      return this.read().then(entry => cb(null, entry));
+      // node's callback overload returns undefined (like close(cb) above)
+      this.read().then(callOnceWithNullThen.bind(null, cb), cb);
+      return;
     }
 
-    if (this.#entries) return Promise.$resolve(this.#entries.shift() ?? null);
+    return this.#enqueue(this.#readOp.bind(this));
+  }
+
+  #opDone() {
+    if (--this.#pendingCount === 0) this.#pendingOp = null;
+  }
 
+  #readOp() {
+    if (this.#handle < 0) throw $ERR_DIR_CLOSED();
+    const entries = this.#entries;
+    if (entries) return this.#entriesIdx < entries.length ? entries[this.#entriesIdx++] : null;
     return fs
       .readdir(this.#path, {
         withFileTypes: true,
         encoding: this.#options?.encoding,
         recursive: this.#options?.recursive,
       })
-      .then(entries => {
-        this.#entries = entries;
-        return entries.shift() ?? null;
-      });
+      .then(this.#onReaddir.bind(this));
+  }
+
+  #onReaddir(entries) {
+    this.#entries = entries;
+    this.#entriesIdx = 0;
+    return this.#entriesIdx < entries.length ? entries[this.#entriesIdx++] : null;
   }
 
-  close(cb?: () => void) {
+  #closeOp() {
     const handle = this.#handle;
     if (handle < 0) throw $ERR_DIR_CLOSED();
+    if (handle > 2) fs.closeSync(handle);
+    this.#handle = -1;
+  }
+
+  close(cb?: (err?: Error) => void) {
     if (!$isUndefinedOrNull(cb)) {
       validateFunction(cb, "callback");
-      process.nextTick(cb);
+      this.close().then(callOnceWithNull.bind(null, cb), cb);
+      return;
     }
-    if (handle > 2) fs.closeSync(handle);
-    this.#handle = -1;
+    return this.#enqueue(this.#closeOp.bind(this));
   }
 
   closeSync() {
     const handle = this.#handle;
     if (handle < 0) throw $ERR_DIR_CLOSED();
+    if (this.#pendingCount > 0) throw this.#dirConcurrentError();
     if (handle > 2) fs.closeSync(handle);
     this.#handle = -1;
   }
 
   get path() {
+    if (!(#path in this)) throw $ERR_INVALID_THIS("Dir");
     return this.#path;
   }
 
   async *[Symbol.asyncIterator]() {
-    let entries = (this.#entries ??= (await fs.readdir(this.#path, {
-      withFileTypes: true,
-      encoding: this.#options?.encoding,
-      recursive: this.#options?.recursive,
-    })) as DirentType[]);
-    yield* entries;
+    try {
+      let entry;
+      while ((entry = await this.read()) !== null) {
+        yield entry;
+      }
+    } finally {
+      // node closes the directory when iteration ends or exits early. Use the
+      // queued async close() so a concurrent in-flight operation doesn't make
+      // teardown throw ERR_DIR_CONCURRENT_OPERATION.
+      if (this.#handle >= 0) await this.close();
+    }
   }
 }
 
@@ -984,9 +1209,20 @@ function glob(pattern: string | string[], options, callback) {
   }
   validateFunction(callback, "callback");
 
-  Array.fromAsync(lazyGlob().glob(pattern, options ?? kEmptyObject))
-    .then(result => callback(null, result))
-    .catch(callback);
+  // Invoke the callback from process.nextTick so that an exception thrown by
+  // the callback surfaces as an uncaught exception instead of rejecting the
+  // internal promise chain (and is never routed back into `callback` as an
+  // error), matching Node.js.
+  Array.fromAsync(lazyGlob().glob(pattern, options ?? kEmptyObject)).then(
+    nextTickWithNullThen.bind(null, callback),
+    nextTickWith.bind(null, callback),
+  );
+}
+function nextTickWithNullThen(callback, result) {
+  process.nextTick(callback, null, result);
+}
+function nextTickWith(callback, err) {
+  process.nextTick(callback, err);
 }
 
 function globSync(pattern: string | string[], options): string[] {
@@ -1042,6 +1278,7 @@ var exports = {
   mkdirSync,
   mkdtemp,
   mkdtempSync,
+  mkdtempDisposableSync,
   open,
   openSync,
   read,
@@ -1142,7 +1379,11 @@ export default exports;
 
 // Preserve the names
 function setName(fn, value) {
-  Object.$defineProperty(fn, "name", { value, enumerable: false, configurable: true });
+  Object.$defineProperty(fn, "name", {
+    value,
+    enumerable: false,
+    configurable: true,
+  });
 }
 setName(Dirent, "Dirent");
 setName(Stats, "Stats");
diff --git a/src/js/node/test.ts b/src/js/node/test.ts
index 65b3c9c468e8..6a75a9768313 100644
--- a/src/js/node/test.ts
+++ b/src/js/node/test.ts
@@ -3,6 +3,7 @@
 
 const { jest } = Bun;
 const { kEmptyObject, throwNotImplemented } = require("internal/shared");
+const { validateBoolean, validateInteger, validateObject } = require("internal/validators");
 
 const kDefaultName = "";
 const kDefaultFunction = () => {};
@@ -12,10 +13,361 @@ function run() {
   throwNotImplemented("run()", 5090, "Use `bun:test` in the interim.");
 }
 
-function mock() {
-  throwNotImplemented("mock()", 5090, "Use `bun:test` in the interim.");
+// Port of Node.js lib/internal/test_runner/mock/mock.js (v26.3.0):
+//   https://github.com/nodejs/node/blob/50c35fea9e64d50ab3bb5f359e8523de89d6c798/lib/internal/test_runner/mock/mock.js
+// API reference: https://nodejs.org/api/test.html#class-mocktracker
+let trackMockCall: (ctx: MockFunctionContext, thisArg: unknown, args: unknown[], target: unknown) => unknown;
+
+class MockFunctionContext {
+  #calls: unknown[];
+  #implementation: Function | undefined;
+  #original: Function;
+  #onceImplementations: Map;
+  #restore: (() => void) | undefined;
+  #times: number;
+
+  constructor(
+    original: Function,
+    implementation: Function | undefined,
+    restore?: () => void,
+    times: number = Infinity,
+  ) {
+    this.#calls = [];
+    this.#original = original;
+    this.#implementation = implementation;
+    this.#onceImplementations = new Map();
+    this.#restore = restore;
+    this.#times = times;
+  }
+
+  get calls() {
+    return Array.from(this.#calls);
+  }
+
+  callCount(): number {
+    return this.#calls.length;
+  }
+
+  mockImplementation(implementation: Function) {
+    if (!$isCallable(implementation)) {
+      throw $ERR_INVALID_ARG_TYPE("implementation", "function", implementation);
+    }
+    this.#implementation = implementation;
+  }
+
+  mockImplementationOnce(implementation: Function, onCall?: number) {
+    if (!$isCallable(implementation)) {
+      throw $ERR_INVALID_ARG_TYPE("implementation", "function", implementation);
+    }
+    // node validates the call index: an integer no earlier than the next call
+    const nextCall = this.#calls.length;
+    const call = onCall ?? nextCall;
+    validateInteger(call, "onCall", nextCall);
+    this.#onceImplementations.set(call, implementation);
+  }
+
+  resetCalls() {
+    this.#calls = [];
+  }
+
+  restore() {
+    // node semantics: a method mock reinstalls the original descriptor but the
+    // context keeps its implementation (calling the detached mock function
+    // still uses it); a bare fn mock reverts to calling the original. Queued
+    // once-implementations survive, and restore() stays re-runnable so a
+    // still-tracked context can be restored again by reset().
+    if (this.#restore !== undefined) {
+      this.#restore();
+    } else {
+      this.#implementation = undefined;
+    }
+  }
+
+  static {
+    trackMockCall = function trackMockCall(
+      ctx: MockFunctionContext,
+      thisArg: unknown,
+      args: unknown[],
+      target: unknown,
+    ) {
+      const callIndex = ctx.#calls.length;
+      let implementation = ctx.#onceImplementations.get(callIndex);
+      if (implementation !== undefined) {
+        ctx.#onceImplementations.delete(callIndex);
+      } else {
+        implementation = ctx.#implementation ?? ctx.#original;
+      }
+      // options.times: revert to the original behavior once the mock has
+      // been used `times` times (node decides this before invoking, so the
+      // current call still uses the mocked implementation).
+      if (callIndex + 1 === ctx.#times) {
+        ctx.restore();
+      }
+      // node records the call in a finally *after* invoking, so a reentrant
+      // implementation observes callCount() === N (not N+1), recursive calls
+      // record in completion order, and the stack is captured post-invoke.
+      let result: unknown;
+      let error: unknown;
+      try {
+        result =
+          target === undefined
+            ? (implementation as Function).$apply(thisArg, args)
+            : Reflect.construct(implementation as Function, args, target as Function);
+        return result;
+      } catch (e) {
+        error = e;
+        throw e;
+      } finally {
+        ctx.#calls.push({
+          arguments: args,
+          error,
+          result,
+          stack: new Error(),
+          target,
+          this: thisArg,
+        });
+      }
+    };
+  }
 }
 
+function createMockFunction(
+  original: Function,
+  implementation: Function | undefined,
+  restore?: () => void,
+  times: number = Infinity,
+) {
+  const context = new MockFunctionContext(original, implementation, restore, times);
+  kMockContexts.push(context);
+  function mockFunction(this: unknown, ...args: unknown[]) {
+    return trackMockCall(context, this, args, new.target);
+  }
+  Object.defineProperty(mockFunction, "mock", {
+    value: context,
+    writable: false,
+    enumerable: false,
+  });
+  Object.defineProperty(mockFunction, "length", {
+    value: original.length,
+    configurable: true,
+  });
+  Object.defineProperty(mockFunction, "name", {
+    value: original.name,
+    configurable: true,
+  });
+  return mockFunction;
+}
+
+const kMockContexts: MockFunctionContext[] = [];
+
+function validateTimes(value: unknown, name: string) {
+  if (value === Infinity) {
+    return;
+  }
+  validateInteger(value, name, 1);
+}
+
+function mockFn(original?: Function | object, implementation?: Function | object, options?: object) {
+  if (original !== null && original !== undefined && !$isCallable(original) && typeof original === "object") {
+    options = implementation as object;
+    implementation = original;
+    original = undefined;
+  }
+  if (
+    implementation !== null &&
+    implementation !== undefined &&
+    !$isCallable(implementation) &&
+    typeof implementation === "object"
+  ) {
+    options = implementation as object;
+    implementation = undefined;
+  }
+  if (original !== undefined && !$isCallable(original)) {
+    throw $ERR_INVALID_ARG_TYPE("original", "function", original);
+  }
+  if (implementation !== undefined && !$isCallable(implementation)) {
+    throw $ERR_INVALID_ARG_TYPE("implementation", "function", implementation);
+  }
+  if (options !== undefined) {
+    validateObject(options, "options");
+  }
+  const { times = Infinity } = (options ?? kEmptyObject) as { times?: number };
+  validateTimes(times, "options.times");
+  return createMockFunction(
+    (original as Function) ?? function () {},
+    implementation as Function | undefined,
+    undefined,
+    times,
+  );
+}
+
+function mockMethod(
+  objectOrFunction: object | Function,
+  methodName: PropertyKey,
+  implementation?: Function | object,
+  options?: { getter?: boolean; setter?: boolean } | object,
+) {
+  if (
+    implementation !== null &&
+    implementation !== undefined &&
+    !$isCallable(implementation) &&
+    typeof implementation === "object"
+  ) {
+    options = implementation;
+    implementation = undefined;
+  }
+  if (implementation !== undefined && !$isCallable(implementation)) {
+    throw $ERR_INVALID_ARG_TYPE("implementation", "function", implementation);
+  }
+  if ((typeof objectOrFunction !== "object" || objectOrFunction === null) && !$isCallable(objectOrFunction)) {
+    throw $ERR_INVALID_ARG_TYPE("object", "object", objectOrFunction);
+  }
+  if (typeof methodName !== "string" && typeof methodName !== "symbol") {
+    throw $ERR_INVALID_ARG_TYPE("methodName", ["string", "symbol"], methodName);
+  }
+  if (options !== undefined) {
+    validateObject(options, "options");
+  }
+  const {
+    getter = false,
+    setter = false,
+    times = Infinity,
+  } = (options ?? kEmptyObject) as {
+    getter?: boolean;
+    setter?: boolean;
+    times?: number;
+  };
+  validateBoolean(getter, "options.getter");
+  validateBoolean(setter, "options.setter");
+  validateTimes(times, "options.times");
+  if (setter && getter) {
+    throw $ERR_INVALID_ARG_VALUE("options.setter", setter, "cannot be used with 'options.getter'");
+  }
+
+  // Find the descriptor on the object or its prototype chain.
+  let target: object | null = objectOrFunction;
+  let descriptor: PropertyDescriptor | undefined;
+  while (target !== null) {
+    descriptor = Object.getOwnPropertyDescriptor(target, methodName);
+    if (descriptor !== undefined) break;
+    target = Object.getPrototypeOf(target);
+  }
+  if (descriptor === undefined) {
+    throw $ERR_INVALID_ARG_VALUE("methodName", methodName, "must be a method");
+  }
+
+  let original: Function;
+  if (getter) {
+    if (!$isCallable(descriptor.get)) {
+      throw $ERR_INVALID_ARG_VALUE("methodName", methodName, "must be a getter");
+    }
+    original = descriptor.get;
+  } else if (setter) {
+    if (!$isCallable(descriptor.set)) {
+      throw $ERR_INVALID_ARG_VALUE("methodName", methodName, "must be a setter");
+    }
+    original = descriptor.set;
+  } else {
+    if (!$isCallable(descriptor.value)) {
+      throw $ERR_INVALID_ARG_VALUE("methodName", methodName, "must be a method");
+    }
+    original = descriptor.value;
+  }
+
+  const restore = function restore() {
+    Object.defineProperty(objectOrFunction, methodName, descriptor!);
+  };
+  const mocked = createMockFunction(original, implementation as Function | undefined, restore, times);
+
+  const mockDescriptor: PropertyDescriptor = {
+    configurable: descriptor.configurable,
+    enumerable: descriptor.enumerable,
+  };
+  if (getter || setter) {
+    if (getter) {
+      mockDescriptor.get = mocked;
+      mockDescriptor.set = descriptor.set;
+    } else {
+      mockDescriptor.get = descriptor.get;
+      mockDescriptor.set = mocked;
+    }
+  } else {
+    mockDescriptor.value = mocked;
+    mockDescriptor.writable = descriptor.writable;
+  }
+  Object.defineProperty(objectOrFunction, methodName, mockDescriptor);
+  return mocked;
+}
+
+const mock = {
+  fn: mockFn,
+  method: mockMethod,
+  getter(
+    objectOrFunction: object | Function,
+    methodName: PropertyKey,
+    implementation?: Function | object,
+    options?: object,
+  ) {
+    // Shift implementation -> options *before* spreading, or the shift inside
+    // mockMethod would clobber the getter flag (node does the same).
+    if (
+      implementation !== null &&
+      implementation !== undefined &&
+      !$isCallable(implementation) &&
+      typeof implementation === "object"
+    ) {
+      options = implementation;
+      implementation = undefined;
+    }
+    const { getter = true } = (options ?? kEmptyObject) as { getter?: boolean };
+    if (getter === false) {
+      throw $ERR_INVALID_ARG_VALUE("options.getter", getter, "cannot be false");
+    }
+    return mockMethod(objectOrFunction, methodName, implementation as Function | undefined, {
+      ...options,
+      getter,
+    });
+  },
+  setter(
+    objectOrFunction: object | Function,
+    methodName: PropertyKey,
+    implementation?: Function | object,
+    options?: object,
+  ) {
+    if (
+      implementation !== null &&
+      implementation !== undefined &&
+      !$isCallable(implementation) &&
+      typeof implementation === "object"
+    ) {
+      options = implementation;
+      implementation = undefined;
+    }
+    const { setter = true } = (options ?? kEmptyObject) as { setter?: boolean };
+    if (setter === false) {
+      throw $ERR_INVALID_ARG_VALUE("options.setter", setter, "cannot be false");
+    }
+    return mockMethod(objectOrFunction, methodName, implementation as Function | undefined, {
+      ...options,
+      setter,
+    });
+  },
+  reset() {
+    // restoreAll() plus disassociating the mocks from the tracker, like node.
+    mock.restoreAll();
+    kMockContexts.length = 0;
+  },
+  restoreAll() {
+    // Restores method mocks to their original descriptor and makes bare
+    // mock.fn() mocks call their original function again, like node. Unlike
+    // reset(), the mocks stay associated with the tracker.
+    for (const ctx of kMockContexts) ctx.restore();
+  },
+  module() {
+    throwNotImplemented("mock.module()", 5090, "Use `bun:test` in the interim.");
+  },
+};
+
 function fileSnapshot(_value: unknown, _path: string, _options: { serializers?: Function[] } = kEmptyObject) {
   throwNotImplemented("fileSnapshot()", 5090, "Use `bun:test` in the interim.");
 }
diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts
index c6d0633dde98..dc6c96a5ca2b 100644
--- a/src/js/node/worker_threads.ts
+++ b/src/js/node/worker_threads.ts
@@ -87,7 +87,9 @@ function injectFakeEmitter(Class) {
   };
 
   Class.prototype.once = function (event, listener) {
-    this.addEventListener(event, functionForEventType(event, listener), { once: true });
+    this.addEventListener(event, functionForEventType(event, listener), {
+      once: true,
+    });
 
     return this;
   };
@@ -117,7 +119,241 @@ const MessagePort = _MessagePort;
 
 let resourceLimits = {};
 
-let workerData = _workerData;
+// Emulation of Node's JSTransferable protocol (kTransfer/kTransferList/kDeserialize) for
+// objects like FileHandle that are not natively transferable in Bun. On send, each such
+// object in the transferList is replaced inside workerData by a serializable marker object;
+// on receive, markers are swapped back for reconstructed instances.
+// A plain string key on purpose: Symbols don't survive structured clone, and
+// Bun has no native HostObject hook, so the marker must ride along inside the
+// cloned graph (including Map/Set entries). This is in-band signaling: a user
+// object that fabricates the key in workerData will deserialize on the worker
+// side where node would deliver it unchanged. That's accepted - it is not a
+// privilege boundary (worker threads share the parent's fd table anyway).
+const kJSTransferableMarker = "__bunNodeWorkerJSTransferable";
+
+function isJSTransferableMarker(value: object): boolean {
+  return (
+    typeof (value as Record)[kJSTransferableMarker] === "string" &&
+    Object.prototype.hasOwnProperty.$call(value, kJSTransferableMarker)
+  );
+}
+
+function deserializeJSTransferable(marker: Record): unknown {
+  const deserializeInfo = marker[kJSTransferableMarker];
+  switch (deserializeInfo) {
+    case "internal/fs/promises:FileHandle": {
+      const { FileHandle, kDeserialize } = require("node:fs").promises.$data;
+      const handle = new FileHandle(-1);
+      handle[kDeserialize](marker.data);
+      return handle;
+    }
+    default:
+      return marker;
+  }
+}
+
+function unpackJSTransferables(value: unknown, memo?: Map): unknown {
+  if (value === null || typeof value !== "object") return value;
+  memo ??= new Map();
+  // The memo both breaks cycles (containers map to themselves) and preserves
+  // reference identity for markers: structured clone keeps a marker shared
+  // between graph positions as one object, so the same marker must
+  // deserialize to the same instance (one FileHandle per transferred fd,
+  // like node's host-object back-references).
+  const cached = memo.get(value);
+  if (cached !== undefined) return cached;
+  if (isJSTransferableMarker(value)) {
+    const instance = deserializeJSTransferable(value as Record);
+    memo.set(value, instance);
+    return instance;
+  }
+  memo.set(value, value);
+  if ($isArray(value)) {
+    for (let i = 0; i < value.length; i++) {
+      // skip holes so sparse arrays stay sparse, like structured clone
+      if (i in value) value[i] = unpackJSTransferables(value[i], memo);
+    }
+    return value;
+  }
+  // Structured clone walks Map/Set entries (keys included), so markers can
+  // arrive inside them; rebuild the entries with deserialized instances.
+  if (value instanceof Map) {
+    const entries: Array<[unknown, unknown]> = [];
+    for (const { 0: k, 1: v } of value) {
+      entries.push([unpackJSTransferables(k, memo), unpackJSTransferables(v, memo)]);
+    }
+    value.clear();
+    for (const { 0: k, 1: v } of entries) value.set(k, v);
+    return value;
+  }
+  if (value instanceof Set) {
+    const items: unknown[] = [];
+    for (const v of value) items.push(unpackJSTransferables(v, memo));
+    value.clear();
+    for (const v of items) value.add(v);
+    return value;
+  }
+  const proto = Object.getPrototypeOf(value);
+  if (proto === Object.prototype || proto === null) {
+    for (const key of Object.keys(value)) {
+      (value as Record)[key] = unpackJSTransferables((value as Record)[key], memo);
+    }
+  }
+  return value;
+}
+
+const kRestoreJSTransferables = Symbol("kRestoreJSTransferables");
+const kFinalizeJSTransferables = Symbol("kFinalizeJSTransferables");
+
+function packJSTransferables(options: NodeWorkerOptions): NodeWorkerOptions {
+  const transferList = options?.transferList;
+  if (!transferList || !$isArray(transferList) || transferList.length === 0) return options;
+  // Avoid loading node:fs for transfer lists that only contain native transferables.
+  let hasCandidate = false;
+  for (const item of transferList) {
+    if (
+      item !== null &&
+      typeof item === "object" &&
+      !(item instanceof ArrayBuffer) &&
+      !(item instanceof _MessagePort) &&
+      !$isTypedArrayView(item)
+    ) {
+      hasCandidate = true;
+      break;
+    }
+  }
+  if (!hasCandidate) return options;
+
+  const { kTransfer, kTransferList, kDeserialize } = require("node:fs").promises.$data;
+  let replacements: Map | undefined;
+  const nativeTransferList: unknown[] = [];
+  // kTransfer() neuters the handle (extracts the bare fd); if anything later
+  // in the pack/construct sequence throws, restore the already-neutered
+  // handles so their fds aren't orphaned.
+  const neutered: Array<[item: any, data: unknown]> = [];
+  function restoreNeutered() {
+    for (const { 0: item, 1: data } of neutered) {
+      try {
+        item[kDeserialize](data);
+      } catch {
+        // best effort - the handle may have been closed concurrently
+      }
+    }
+  }
+  try {
+    for (const item of transferList) {
+      if (item !== null && typeof item === "object" && typeof item[kTransfer] === "function") {
+        if (replacements?.has(item)) {
+          // node (and the HTML spec) reject duplicate transferList entries;
+          // without this the second kTransfer() would read the already
+          // neutered fd (-1) and clobber the real marker.
+          throw new DOMException(
+            `Transfer list contains duplicate ${item.constructor?.name ?? "entry"}`,
+            "DataCloneError",
+          );
+        }
+        const extraTransfers = item[kTransferList]?.();
+        // May throw DataCloneError (e.g. FileHandle in use); propagate synchronously like Node.
+        const { data, deserializeInfo } = item[kTransfer]();
+        neutered.push([item, data]);
+        (replacements ??= new Map()).set(item, {
+          [kJSTransferableMarker]: deserializeInfo,
+          data,
+        });
+        if ($isArray(extraTransfers)) nativeTransferList.push(...extraTransfers);
+      } else {
+        nativeTransferList.push(item);
+      }
+    }
+  } catch (e) {
+    restoreNeutered();
+    throw e;
+  }
+  if (!replacements) return options;
+
+  const seen = new Map();
+  const usedMarkers = new Set();
+  function replace(value: unknown): unknown {
+    if (value === null || typeof value !== "object") return value;
+    const replacement = replacements!.get(value);
+    if (replacement !== undefined) {
+      usedMarkers.add(value);
+      return replacement;
+    }
+    const cached = seen.get(value);
+    if (cached !== undefined) return cached;
+    if ($isArray(value)) {
+      const out = new Array(value.length);
+      seen.set(value, out);
+      // skip holes so sparse arrays stay sparse, like structured clone
+      for (let i = 0; i < value.length; i++) {
+        if (i in value) out[i] = replace(value[i]);
+      }
+      return out;
+    }
+    // Mirror structured clone: Map/Set entries (keys included) participate
+    // in the graph, so a transferred handle inside them must become its
+    // marker rather than being orphaned.
+    if (value instanceof Map) {
+      const out = new Map();
+      seen.set(value, out);
+      for (const { 0: k, 1: v } of value) out.set(replace(k), replace(v));
+      return out;
+    }
+    if (value instanceof Set) {
+      const out = new Set();
+      seen.set(value, out);
+      for (const v of value) out.add(replace(v));
+      return out;
+    }
+    const proto = Object.getPrototypeOf(value);
+    if (proto === Object.prototype || proto === null) {
+      const out: Record = {};
+      seen.set(value, out);
+      for (const key of Object.keys(value)) out[key] = replace((value as Record)[key]);
+      return out;
+    }
+    return value;
+  }
+  // replace() reads property getters and Proxy traps (Object.keys,
+  // Object.getPrototypeOf, value[key]), and the options spread reads
+  // getters on the user's options object — any of which can throw after
+  // handles are already neutered. Roll back here too so a throwing
+  // workerData graph doesn't orphan the fds it transferred.
+  let packed;
+  try {
+    packed = {
+      ...options,
+      workerData: replace(options.workerData),
+      transferList: nativeTransferList,
+    };
+  } catch (e) {
+    restoreNeutered();
+    throw e;
+  }
+  packed[kRestoreJSTransferables] = restoreNeutered;
+  // A handle in transferList but never referenced from workerData is still
+  // detached from this thread (fd === -1, like node), but no marker will
+  // deserialize it on the worker side - close the orphaned fd instead of
+  // leaking it (node's worker-side instance is reclaimed by GC). This runs
+  // only after WebWorker construction succeeds: if construction throws, the
+  // rollback above must still find the fd open to restore the handle (node
+  // leaves the handle fully usable in that case).
+  packed[kFinalizeJSTransferables] = function finalizeJSTransferables() {
+    for (const { 0: item, 1: data } of neutered) {
+      if (!usedMarkers.has(item) && typeof (data as any)?.fd === "number" && (data as any).fd >= 0) {
+        try {
+          require("node:fs").closeSync((data as any).fd);
+        } catch {
+          // already closed
+        }
+      }
+    }
+  };
+  return packed;
+}
+
+let workerData = unpackJSTransferables(_workerData);
 let threadId = _threadId;
 function receiveMessageOnPort(port: MessagePort) {
   let res = _receiveMessageOnPort(port);
@@ -235,6 +471,8 @@ class Worker extends EventEmitter {
   constructor(filename: string, options: NodeWorkerOptions = {}) {
     super();
 
+    options = packJSTransferables(options);
+
     const builtinsGeneratorHatesEval = "ev" + "a" + "l"[0];
     if (options && builtinsGeneratorHatesEval in options) {
       if (options[builtinsGeneratorHatesEval]) {
@@ -251,16 +489,26 @@ class Worker extends EventEmitter {
     try {
       this.#worker = new WebWorker(filename, options as Bun.WorkerOptions, this);
     } catch (e) {
+      // Restore any transferList handles that were already neutered by
+      // packJSTransferables, so their fds aren't orphaned.
+      options[kRestoreJSTransferables]?.();
       if (this.#urlToRevoke) {
         URL.revokeObjectURL(this.#urlToRevoke);
       }
       throw e;
     }
-    this.#worker.addEventListener("close", this.#onClose.bind(this), { once: true });
+    // The transfer is committed - release fds that were transferred but are
+    // not referenced from workerData (nothing will deserialize them).
+    options[kFinalizeJSTransferables]?.();
+    this.#worker.addEventListener("close", this.#onClose.bind(this), {
+      once: true,
+    });
     this.#worker.addEventListener("error", this.#onError.bind(this));
     this.#worker.addEventListener("message", this.#onMessage.bind(this));
     this.#worker.addEventListener("messageerror", this.#onMessageError.bind(this));
-    this.#worker.addEventListener("open", this.#onOpen.bind(this), { once: true });
+    this.#worker.addEventListener("open", this.#onOpen.bind(this), {
+      once: true,
+    });
 
     if (this.#urlToRevoke) {
       if (!urlRevokeRegistry) {
diff --git a/src/jsc/ErrorCode.rs b/src/jsc/ErrorCode.rs
index bac8a2c5288a..ac8c10a52b18 100644
--- a/src/jsc/ErrorCode.rs
+++ b/src/jsc/ErrorCode.rs
@@ -703,9 +703,15 @@ impl ErrorCode {
     pub const TLS_ALPN_CALLBACK_INVALID_RESULT: ErrorCode = ErrorCode(322);
     /// `ERR_PROXY_TUNNEL` (instanceof Error)
     pub const PROXY_TUNNEL: ErrorCode = ErrorCode(323);
+    /// `ERR_FS_CP_EEXIST` (instanceof Error)
+    pub const FS_CP_EEXIST: ErrorCode = ErrorCode(324);
+    /// `ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY` (instanceof Error)
+    pub const FS_CP_SYMLINK_TO_SUBDIRECTORY: ErrorCode = ErrorCode(325);
+    /// `ERR_DIR_CONCURRENT_OPERATION` (instanceof Error)
+    pub const DIR_CONCURRENT_OPERATION: ErrorCode = ErrorCode(326);
 
     /// == C++ `NODE_ERROR_COUNT`.
-    pub const COUNT: u16 = 324;
+    pub const COUNT: u16 = 327;
 }
 
 // ──────────────────────────────────────────────────────────────────────────
@@ -773,6 +779,7 @@ impl ErrorCode {
     pub const ERR_CRYPTO_UNKNOWN_DH_GROUP: ErrorCode = ErrorCode::CRYPTO_UNKNOWN_DH_GROUP;
     pub const ERR_CRYPTO_UNSUPPORTED_OPERATION: ErrorCode = ErrorCode::CRYPTO_UNSUPPORTED_OPERATION;
     pub const ERR_DIR_CLOSED: ErrorCode = ErrorCode::DIR_CLOSED;
+    pub const ERR_DIR_CONCURRENT_OPERATION: ErrorCode = ErrorCode::DIR_CONCURRENT_OPERATION;
     pub const ERR_DLOPEN_DISABLED: ErrorCode = ErrorCode::DLOPEN_DISABLED;
     pub const ERR_DLOPEN_FAILED: ErrorCode = ErrorCode::DLOPEN_FAILED;
     pub const ERR_DNS_SET_SERVERS_FAILED: ErrorCode = ErrorCode::DNS_SET_SERVERS_FAILED;
@@ -786,10 +793,13 @@ impl ErrorCode {
         ErrorCode::FEATURE_UNAVAILABLE_ON_PLATFORM;
     pub const ERR_FORMDATA_PARSE_ERROR: ErrorCode = ErrorCode::FORMDATA_PARSE_ERROR;
     pub const ERR_FS_CP_DIR_TO_NON_DIR: ErrorCode = ErrorCode::FS_CP_DIR_TO_NON_DIR;
+    pub const ERR_FS_CP_EEXIST: ErrorCode = ErrorCode::FS_CP_EEXIST;
     pub const ERR_FS_CP_EINVAL: ErrorCode = ErrorCode::FS_CP_EINVAL;
     pub const ERR_FS_CP_FIFO_PIPE: ErrorCode = ErrorCode::FS_CP_FIFO_PIPE;
     pub const ERR_FS_CP_NON_DIR_TO_DIR: ErrorCode = ErrorCode::FS_CP_NON_DIR_TO_DIR;
     pub const ERR_FS_CP_SOCKET: ErrorCode = ErrorCode::FS_CP_SOCKET;
+    pub const ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY: ErrorCode =
+        ErrorCode::FS_CP_SYMLINK_TO_SUBDIRECTORY;
     pub const ERR_FS_CP_UNKNOWN: ErrorCode = ErrorCode::FS_CP_UNKNOWN;
     pub const ERR_FS_EISDIR: ErrorCode = ErrorCode::FS_EISDIR;
     pub const ERR_HTTP_BODY_NOT_ALLOWED: ErrorCode = ErrorCode::HTTP_BODY_NOT_ALLOWED;
@@ -1412,6 +1422,9 @@ static CODE_STR: [&str; ErrorCode::COUNT as usize] = [
     "ERR_HTTP2_GOAWAY_SESSION",
     "ERR_TLS_ALPN_CALLBACK_INVALID_RESULT",
     "ERR_PROXY_TUNNEL",
+    "ERR_FS_CP_EEXIST",
+    "ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY",
+    "ERR_DIR_CONCURRENT_OPERATION",
 ];
 
 // ──────────────────────────────────────────────────────────────────────────
diff --git a/src/jsc/bindings/ErrorCode.ts b/src/jsc/bindings/ErrorCode.ts
index b6f42bfd8dd8..a35e294457cf 100644
--- a/src/jsc/bindings/ErrorCode.ts
+++ b/src/jsc/bindings/ErrorCode.ts
@@ -335,5 +335,8 @@ const errors: ErrorCodeMapping = [
   ["ERR_HTTP2_GOAWAY_SESSION", Error],
   ["ERR_TLS_ALPN_CALLBACK_INVALID_RESULT", TypeError],
   ["ERR_PROXY_TUNNEL", Error],
+  ["ERR_FS_CP_EEXIST", Error],
+  ["ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY", Error],
+  ["ERR_DIR_CONCURRENT_OPERATION", Error],
 ];
 export default errors;
diff --git a/src/jsc/bindings/NodeDirent.cpp b/src/jsc/bindings/NodeDirent.cpp
index f6667116545c..d1af85114c28 100644
--- a/src/jsc/bindings/NodeDirent.cpp
+++ b/src/jsc/bindings/NodeDirent.cpp
@@ -169,7 +169,6 @@ JSC_DEFINE_HOST_FUNCTION(constructDirent, (JSC::JSGlobalObject * globalObject, J
     auto* originalStructure = structure;
     JSValue newTarget = callFrame->newTarget();
     if (zigGlobalObject->m_JSDirentClassStructure.constructor(zigGlobalObject) != newTarget) [[unlikely]] {
-        auto scope = DECLARE_THROW_SCOPE(vm);
         if (!newTarget) {
             throwTypeError(globalObject, scope, "Class constructor Dirent cannot be invoked without 'new'"_s);
             return {};
diff --git a/src/jsc/bindings/NodeValidator.cpp b/src/jsc/bindings/NodeValidator.cpp
index 9e6aa3b6d41c..5d11fe7b7c66 100644
--- a/src/jsc/bindings/NodeValidator.cpp
+++ b/src/jsc/bindings/NodeValidator.cpp
@@ -363,7 +363,9 @@ JSC::EncodedJSValue V::validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject
 
     if (minLength.isUndefined()) minLength = jsNumber(0);
 
-    if (!JSC::isArray(globalObject, value)) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Array"_s, value);
+    bool isArray = JSC::isArray(globalObject, value);
+    RETURN_IF_EXCEPTION(scope, {});
+    if (!isArray) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Array"_s, value);
 
     auto length = value.get(globalObject, Identifier::fromString(vm, "length"_s));
     RETURN_IF_EXCEPTION(scope, {});
@@ -382,7 +384,9 @@ JSC::EncodedJSValue V::validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject
 
     if (minLength.isUndefined()) minLength = jsNumber(0);
 
-    if (!JSC::isArray(globalObject, value)) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Array"_s, value);
+    bool isArray = JSC::isArray(globalObject, value);
+    RETURN_IF_EXCEPTION(scope, {});
+    if (!isArray) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Array"_s, value);
 
     auto length = value.get(globalObject, Identifier::fromString(vm, "length"_s));
     RETURN_IF_EXCEPTION(scope, {});
@@ -704,7 +708,9 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateObject, (JSC::JSGlobalObject * globa
 
     auto value = callFrame->argument(0);
 
-    if (value.isNull() || JSC::isArray(globalObject, value) || value.isCallable()) {
+    bool isArray = JSC::isArray(globalObject, value);
+    RETURN_IF_EXCEPTION(scope, {});
+    if (value.isNull() || isArray || value.isCallable()) {
         return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, callFrame->argument(1), "object"_s, value);
     }
 
@@ -717,7 +723,9 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateObject, (JSC::JSGlobalObject * globa
 
 JSC::EncodedJSValue V::validateObject(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name)
 {
-    if (value.isNull() || JSC::isArray(globalObject, value) || value.isCallable()) {
+    bool isArray = JSC::isArray(globalObject, value);
+    RETURN_IF_EXCEPTION(scope, {});
+    if (value.isNull() || isArray || value.isCallable()) {
         return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "object"_s, value);
     }
 
diff --git a/src/jsc/modules/NodeProcessModule.h b/src/jsc/modules/NodeProcessModule.h
index 6a0ce5818f7b..80ac581d48c2 100644
--- a/src/jsc/modules/NodeProcessModule.h
+++ b/src/jsc/modules/NodeProcessModule.h
@@ -13,10 +13,11 @@ DEFINE_NATIVE_MODULE(NodeProcess)
     auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
 
     Bun::Process* process = globalObject->processObject();
-    if (!process->staticPropertiesReified()) {
-        process->reifyAllStaticProperties(globalObject);
-        RETURN_IF_EXCEPTION(scope, );
-    }
+    // Don't bulk-reifyAllStaticProperties here (see generateNativeModule_NodeModule
+    // for the long version). It runs every PropertyCallback back-to-back without an
+    // exception check in between, which trips BUN_JSC_validateExceptionChecks=1.
+    // The per-export get() below lazy-reifies one property at a time inside
+    // JSObject::get's own checked ThrowScope.
 
     PropertyNameArrayBuilder properties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
     process->getPropertyNames(globalObject, properties, DontEnumPropertiesMode::Exclude);
diff --git a/src/runtime/node/path_watcher.rs b/src/runtime/node/path_watcher.rs
index 9e33639dbba3..87a529967519 100644
--- a/src/runtime/node/path_watcher.rs
+++ b/src/runtime/node/path_watcher.rs
@@ -246,12 +246,11 @@ impl EventType {
 
 /// Per-handler duplicate suppression.
 ///
-/// The predicate is intentionally identical to `win_watcher.rs`
-/// so POSIX and Windows agree on which bursts are coalesced.
-/// It suppresses only when, within the same millisecond, *both* the hash and
-/// the event type match the previous emission — arguably too aggressive, but
-/// changing it here would diverge from Windows; fixing all three together is
-/// a separate change.
+/// Suppresses only exact duplicates: same path hash *and* same event type
+/// within a 1ms window. Distinct files changed in the same millisecond must
+/// each emit — node delivers both (see test/js/node/test/parallel
+/// fs-watch tests that write two files back-to-back). Kept identical to
+/// `win_watcher.rs` so POSIX and Windows agree on which bursts are coalesced.
 #[derive(Default)]
 pub(crate) struct ChangeEvent {
     #[cfg(not(windows))]
@@ -266,8 +265,10 @@ pub(crate) struct ChangeEvent {
 impl ChangeEvent {
     fn should_emit(&mut self, hash: u64, timestamp: i64, event_type: EventType) -> bool {
         let time_diff = timestamp - self.timestamp;
-        if (self.timestamp == 0 || time_diff > 1)
-            || (self.event_type_ != event_type && self.hash != hash)
+        if self.timestamp == 0
+            || time_diff > 1
+            || self.event_type_ != event_type
+            || self.hash != hash
         {
             self.timestamp = timestamp;
             self.event_type_ = event_type;
@@ -1054,11 +1055,28 @@ impl Linux {
                             &[watcher_path, owner_subpath, name],
                         );
                         // Borrowck: `rel` may borrow `path_buf`,
-                        // which `walk_and_add` also borrows. Own it for the call.
+                        // which `walk_subtree` also borrows. Own it for the call.
                         let rel_owned: Box<[u8]> = Box::from(rel);
                         // These may rehash `wd_map`; `owners` is re-fetched next iteration.
                         let _ = Linux::add_one(manager, watcher, child_abs, &rel_owned);
-                        Linux::walk_and_add(manager, watcher, child_abs, &rel_owned);
+                        // Entries created inside the new directory before our watch
+                        // attached never get their own IN_CREATE on this fd. Walk the
+                        // subtree: watch nested directories and synthesize a "rename"
+                        // for every discovered entry, like node's recursive watcher
+                        // does when it scans a newly added folder
+                        // (lib/internal/fs/recursive_watch.js). An entry created after
+                        // the watch attached may emit twice; per-handler ChangeEvent
+                        // coalescing absorbs back-to-back duplicates.
+                        walk_subtree::(
+                            child_abs,
+                            &rel_owned,
+                            &mut |abs, entry_rel, entry_is_file| {
+                                if !entry_is_file {
+                                    let _ = Linux::add_one(manager, watcher, abs, entry_rel);
+                                }
+                                watcher.emit(EventType::Rename, entry_rel, entry_is_file);
+                            },
+                        );
                     }
 
                     oi += 1;
diff --git a/src/runtime/node/win_watcher.rs b/src/runtime/node/win_watcher.rs
index f5e95022b99e..7f5a2d44c688 100644
--- a/src/runtime/node/win_watcher.rs
+++ b/src/runtime/node/win_watcher.rs
@@ -175,9 +175,11 @@ impl ChangeEvent {
         event_type: EventType,
     ) -> bool {
         let time_diff = timestamp.saturating_sub(self.timestamp);
-        // skip consecutive duplicates
-        if (self.timestamp == 0 || time_diff > 1)
-            || (self.event_type != event_type && self.hash != hash)
+        // skip consecutive exact duplicates (same path and event type) only
+        if self.timestamp == 0
+            || time_diff > 1
+            || self.event_type != event_type
+            || self.hash != hash
         {
             self.timestamp = timestamp;
             self.event_type = event_type;
diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts
index eb6e4dc028ab..c851331e3c29 100644
--- a/test/bundler/expectBundled.ts
+++ b/test/bundler/expectBundled.ts
@@ -13,7 +13,6 @@ import {
   readdirSync,
   readFileSync,
   realpathSync,
-  rmdirSync,
   rmSync,
   writeFileSync,
 } from "fs";
@@ -1811,7 +1810,7 @@ for (const [key, blob] of build.outputs) {
       }
     }
 
-    rmdirSync(root, { recursive: true });
+    rmSync(root, { recursive: true, force: true });
 
     return testRef(id, opts);
   })();
diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts
index 41315f703108..e4f7b7e30030 100644
--- a/test/cli/inspect/inspect.test.ts
+++ b/test/cli/inspect/inspect.test.ts
@@ -312,7 +312,7 @@ describe("unix domain socket without websocket", () => {
   });
 
   afterAll(() => {
-    fs.rmdirSync(tempdir, { recursive: true });
+    fs.rmSync(tempdir, { recursive: true, force: true });
   });
 
   if (isPosix) {
diff --git a/test/fixtures/copy/kitchen-sink/README.md b/test/fixtures/copy/kitchen-sink/README.md
new file mode 100644
index 000000000000..fec56017dc1b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/README.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/b/README2.md b/test/fixtures/copy/kitchen-sink/a/b/README2.md
new file mode 100644
index 000000000000..fec56017dc1b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/b/README2.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/b/index.js b/test/fixtures/copy/kitchen-sink/a/b/index.js
new file mode 100644
index 000000000000..12388b0457bd
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/b/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/c/README2.md b/test/fixtures/copy/kitchen-sink/a/c/README2.md
new file mode 100644
index 000000000000..fec56017dc1b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/README2.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/c/d/README3.md b/test/fixtures/copy/kitchen-sink/a/c/d/README3.md
new file mode 100644
index 000000000000..fec56017dc1b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/d/README3.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/c/d/index.js b/test/fixtures/copy/kitchen-sink/a/c/d/index.js
new file mode 100644
index 000000000000..12388b0457bd
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/d/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/c/index.js b/test/fixtures/copy/kitchen-sink/a/c/index.js
new file mode 100644
index 000000000000..12388b0457bd
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/index.js b/test/fixtures/copy/kitchen-sink/a/index.js
new file mode 100644
index 000000000000..12388b0457bd
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/index.js b/test/fixtures/copy/kitchen-sink/index.js
new file mode 100644
index 000000000000..12388b0457bd
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  purpose: 'testing copy'
+};
diff --git a/test/js/bun/bun-object/write.spec.ts b/test/js/bun/bun-object/write.spec.ts
index 05457c95f0d2..c4978f18eb88 100644
--- a/test/js/bun/bun-object/write.spec.ts
+++ b/test/js/bun/bun-object/write.spec.ts
@@ -52,7 +52,7 @@ describe("Bun.write() on file paths", () => {
 
   afterAll(async () => {
     console.log("%%BUNWRITE ON FILE PATHS%% AFTER ALL");
-    await fs.rmdir(dir, { recursive: true });
+    await fs.rm(dir, { recursive: true, force: true });
   });
 
   describe("Given a path to a file in an existing directory", () => {
@@ -117,7 +117,7 @@ describe("Bun.write() on file paths", () => {
       filepath = path.join(rootdir, "bar/baz", "test-file.txt");
     });
     afterEach(async () => {
-      await fs.rmdir(rootdir, { recursive: true }).catch(() => {});
+      await fs.rm(rootdir, { recursive: true, force: true }).catch(() => {});
     });
 
     describe("When no options are provided", () => {
@@ -168,7 +168,7 @@ describe("Bun.write() on BunFiles", () => {
   });
 
   afterAll(async () => {
-    await fs.rmdir(dir, { recursive: true });
+    await fs.rm(dir, { recursive: true, force: true });
   });
 
   describe("Given a text file that exists", () => {
diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts
index aa2b6e01e609..0808cbc03c1e 100644
--- a/test/js/node/fs/cp.test.ts
+++ b/test/js/node/fs/cp.test.ts
@@ -42,8 +42,10 @@ for (const [name, copy] of impls) {
       });
 
       const e = await copyShouldThrow(basename + "/from", basename + "/result");
-      expect(e.code).toBe("EISDIR");
-      expect(e.path).toBe(join(basename, "from"));
+      expect(e.code).toBe("ERR_FS_EISDIR");
+      // The path field echoes the caller's string verbatim (node does not
+      // resolve or normalize it), so expect the same concatenation we passed.
+      expect(e.path).toBe(basename + "/from");
     });
 
     test("recursive directory structure - no destination", async () => {
@@ -136,8 +138,9 @@ for (const [name, copy] of impls) {
         force: false,
         errorOnExist: true,
       });
-      expect(e.code).toBe("EEXIST");
-      expect(e.path).toBe(join(basename, "result", "a.txt"));
+      expect(e.code).toBe("ERR_FS_CP_EEXIST");
+      // As above, the path field carries the caller's string verbatim.
+      expect(e.path).toBe(basename + "/result/a.txt");
 
       assertContent(basename + "/result/a.txt", "win");
     });
@@ -267,6 +270,10 @@ for (const [name, copy] of impls) {
 
       await copy(basename + "/from", basename + "/result", {
         filter: (src: string) => {
+          // cp joins child paths with the platform separator, so on Windows
+          // the filter sees backslash-separated paths; normalize for the
+          // assertion.
+          src = src.replaceAll("\\", "/");
           return src.endsWith("/from") || src.includes("a.txt");
         },
         recursive: true,
@@ -353,7 +360,15 @@ for (const [name, copy] of impls) {
         "hey": "hi",
       });
 
-      await copy(basename + "/hey", basename + "/hey");
+      // node rejects copying a file onto itself with ERR_FS_CP_EINVAL;
+      // the regression this guards against is throwing EBUSY instead.
+      let err: any;
+      try {
+        await copy(basename + "/hey", basename + "/hey");
+      } catch (e) {
+        err = e;
+      }
+      expect(err?.code).toBe("ERR_FS_CP_EINVAL");
     });
   });
 }
@@ -520,7 +535,7 @@ test.skipIf(!isPosix)(
             console.log("UNEXPECTED-SUCCESS");
             process.exit(1);
           } catch (e) {
-            if (e?.code !== "EISDIR") {
+            if (e?.code !== "ERR_FS_CP_NON_DIR_TO_DIR") {
               console.log("UNEXPECTED-ERROR:" + (e?.code ?? e?.message));
               process.exit(1);
             }
diff --git a/test/js/node/fs/dir.test.ts b/test/js/node/fs/dir.test.ts
index b85782984f50..2780aea96ebc 100644
--- a/test/js/node/fs/dir.test.ts
+++ b/test/js/node/fs/dir.test.ts
@@ -16,6 +16,24 @@ describe("fs.opendir", () => {
   it("throws if callback is not provided", () => {
     expect(() => fs.opendir("foo")).toThrow(/The "callback" argument must be of type function/);
   });
+
+  it("opendirSync on a file throws ENOTDIR with libuv's platform errno", () => {
+    const file = path.join(os.tmpdir(), "opendir-enotdir-" + String(Math.random() * 100).substring(0, 6) + ".txt");
+    fs.writeFileSync(file, "not a directory");
+    try {
+      let err: any;
+      try {
+        fs.opendirSync(file);
+      } catch (e) {
+        err = e;
+      }
+      expect(err?.code).toBe("ENOTDIR");
+      expect(err?.errno).toBe(process.platform === "win32" ? -4052 : -20);
+      expect(err?.syscall).toBe("opendir");
+    } finally {
+      fs.rmSync(file, { force: true });
+    }
+  });
 });
 
 describe("fs.Dir", () => {
@@ -29,7 +47,7 @@ describe("fs.Dir", () => {
     });
 
     afterAll(() => {
-      fs.rmdirSync(dirname, { recursive: true });
+      fs.rmSync(dirname, { recursive: true, force: true });
     });
 
     describe("when an empty directory is opened", () => {
@@ -117,3 +135,69 @@ describe("fs.Dir", () => {
     }); // 
   }); // 
 }); // 
+
+describe("fs.opendir async validation", () => {
+  it("does not invoke the callback synchronously", async () => {
+    const dirname = path.join(os.tmpdir(), "opendir-async-" + String(Math.random() * 100).substring(0, 6));
+    fs.mkdirSync(dirname);
+    try {
+      let sync = true;
+      const { promise, resolve } = Promise.withResolvers();
+      fs.opendir(dirname, (err, dir) => {
+        resolve(sync);
+        dir?.close(() => {});
+      });
+      sync = false;
+      expect(await promise).toBe(false);
+    } finally {
+      fs.rmSync(dirname, { recursive: true, force: true });
+    }
+  });
+
+  it("reports ENOTDIR through the callback, not a synchronous throw", async () => {
+    const file = path.join(os.tmpdir(), "opendir-async-file-" + String(Math.random() * 100).substring(0, 6));
+    fs.writeFileSync(file, "x");
+    try {
+      const { promise, resolve } = Promise.withResolvers();
+      fs.opendir(file, err => resolve(err));
+      const err = await promise;
+      expect(err?.code).toBe("ENOTDIR");
+      expect(err?.syscall).toBe("opendir");
+    } finally {
+      fs.rmSync(file, { force: true });
+    }
+  });
+});
+
+describe("opendirSync string encoding shorthand", () => {
+  it("validates a string options argument as an encoding", () => {
+    const dirname = path.join(os.tmpdir(), "opendir-enc-" + String(Math.random() * 100).substring(0, 6));
+    fs.mkdirSync(dirname);
+    try {
+      // an invalid encoding passed as the shorthand is validated like node
+      expect(() => fs.opendirSync(dirname, "nope")).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }));
+    } finally {
+      fs.rmSync(dirname, { recursive: true, force: true });
+    }
+  });
+
+  // On Windows the native readdir always emits UTF-8 names (a pre-existing
+  // gap: fs.readdirSync ignores the encoding option there too), so the
+  // byte-reinterpretation is only observable on POSIX.
+  it.skipIf(process.platform === "win32")("applies the encoding to entry names", () => {
+    const dirname = path.join(os.tmpdir(), "opendir-enc-" + String(Math.random() * 100).substring(0, 6));
+    fs.mkdirSync(dirname);
+    // latin1 makes the shorthand observable: the utf8 bytes of the name are
+    // reinterpreted per-byte. (encoding: "buffer" dirents are a pre-existing
+    // native readdir gap unrelated to the shorthand.)
+    fs.writeFileSync(path.join(dirname, "na\u00efve.txt"), "x");
+    try {
+      const dir = fs.opendirSync(dirname, "latin1");
+      const entry = dir.readSync();
+      expect(entry?.name).toBe(Buffer.from("na\u00efve.txt", "utf8").toString("latin1"));
+      dir.closeSync();
+    } finally {
+      fs.rmSync(dirname, { recursive: true, force: true });
+    }
+  });
+});
diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts
index 4122f0003878..2f53d8c0704c 100644
--- a/test/js/node/fs/fs.test.ts
+++ b/test/js/node/fs/fs.test.ts
@@ -1941,7 +1941,7 @@ describe("rm", () => {
 
   // On Windows a leading-separator, drive-less path like "/foo/bar" is
   // "rooted" and must be resolved against the cwd's drive. existsSync/
-  // statSync/unlinkSync all do this; recursive rmSync/rmdirSync must agree
+  // statSync/unlinkSync all do this; recursive rmSync must agree
   // or cleanup helpers (rmSync(dir, { recursive: true, force: true })) silently
   // no-op on directories existsSync just said were there.
   //
@@ -1965,14 +1965,6 @@ describe("rm", () => {
     fs.rmSync(dir, { recursive: true, force: true });
     expect(fs.existsSync(dir)).toBe(false);
   });
-
-  it.skipIf(!sameDriveAsCwd)("rmdirSync recursive agrees with existsSync for rooted POSIX-style paths", () => {
-    const dir = `${drivelessTmp}/bun-rmdir-posix-path-${Date.now()}-${Math.random().toString(36).slice(2)}`;
-    fs.mkdirSync(dir + "/nested", { recursive: true });
-    expect(fs.existsSync(dir)).toBe(true);
-    fs.rmdirSync(dir, { recursive: true });
-    expect(fs.existsSync(dir)).toBe(false);
-  });
 });
 
 describe("rmdir", () => {
@@ -2039,25 +2031,21 @@ describe("rmdir", () => {
 
     expect(existsSync(path + "/file.txt")).toBe(true);
 
-    await promises.rmdir(path, { recursive: true });
+    await expect(promises.rmdir(path, { recursive: true })).rejects.toMatchObject({ code: "ERR_INVALID_ARG_VALUE" });
+    await promises.rm(path, { recursive: true, force: true });
     expect(existsSync(path + "/file.txt")).toBe(false);
   });
-  it("removes a dir recursively", done => {
+  it("throws for recursive: true like node", () => {
     const path = `${tmpdir()}/${Date.now()}.rm.dir/foo/bar`;
     try {
       mkdirSync(path, { recursive: true });
     } catch (e) {}
     expect(existsSync(path)).toBe(true);
-    rmdir(join(path, "../../"), { recursive: true }, err => {
-      try {
-        expect(existsSync(path)).toBe(false);
-        done(err);
-      } catch (e) {
-        return done(e);
-      } finally {
-        done();
-      }
-    });
+    expect(() => {
+      rmdir(join(path, "../../"), { recursive: true }, () => {});
+    }).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }));
+    rmSync(join(path, "../../"), { recursive: true, force: true });
+    expect(existsSync(path)).toBe(false);
   });
 });
 
@@ -2080,13 +2068,16 @@ describe("rmdirSync", () => {
     rmdirSync(path);
     expect(existsSync(path)).toBe(false);
   });
-  it("removes a dir recursively", () => {
+  it("throws for recursive: true like node", () => {
     const path = `${tmpdir()}/${Date.now()}.rm.dir/foo/bar`;
     try {
       mkdirSync(path, { recursive: true });
     } catch (e) {}
     expect(existsSync(path)).toBe(true);
-    rmdirSync(join(path, "../../"), { recursive: true });
+    expect(() => rmdirSync(join(path, "../../"), { recursive: true })).toThrow(
+      expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }),
+    );
+    rmSync(join(path, "../../"), { recursive: true, force: true });
     expect(existsSync(path)).toBe(false);
   });
 });
@@ -2576,7 +2567,7 @@ describe("createWriteStream", () => {
 });
 
 describe("fs/promises", () => {
-  const { exists, mkdir, readFile, rmdir, stat, writeFile } = promises;
+  const { exists, mkdir, readFile, rm, rmdir, stat, writeFile } = promises;
 
   it("should not segfault on exception", async () => {
     try {
@@ -3003,13 +2994,16 @@ describe("fs/promises", () => {
       await rmdir(path);
       expect(await exists(path)).toBe(false);
     });
-    it("removes a dir recursively", async () => {
+    it("throws for recursive: true like node", async () => {
       const path = `${tmpdir()}/${Date.now()}.rm.dir/foo/bar`;
       try {
         await mkdir(path, { recursive: true });
       } catch (e) {}
       expect(await exists(path)).toBe(true);
-      await rmdir(join(path, "../../"), { recursive: true });
+      await expect(rmdir(join(path, "../../"), { recursive: true })).rejects.toMatchObject({
+        code: "ERR_INVALID_ARG_VALUE",
+      });
+      await rm(join(path, "../../"), { recursive: true, force: true });
       expect(await exists(path)).toBe(false);
     });
   });
diff --git a/test/js/node/fs/glob.test.ts b/test/js/node/fs/glob.test.ts
index 7544a36ea6e7..8314f9cf60fa 100644
--- a/test/js/node/fs/glob.test.ts
+++ b/test/js/node/fs/glob.test.ts
@@ -24,7 +24,7 @@ beforeAll(() => {
 });
 
 afterAll(() => {
-  return fs.promises.rmdir(tmp, { recursive: true });
+  return fs.promises.rm(tmp, { recursive: true, force: true });
 });
 
 describe("fs.glob", () => {
@@ -48,12 +48,12 @@ describe("fs.glob", () => {
 
   it("can filter out files", done => {
     const exclude = (path: string) => path.endsWith(".js");
-    fs.glob("a/*", { cwd: tmp, exclude }, (err, paths) => {
+    fs.glob("a/**", { cwd: tmp, exclude }, (err, paths) => {
       if (err) done(err);
       if (isWindows) {
-        expect(paths).toStrictEqual(["a\\bar.txt"]);
+        expect(paths.sort()).toStrictEqual(["a", "a\\bar.txt"]);
       } else {
-        expect(paths).toStrictEqual(["a/bar.txt"]);
+        expect(paths.sort()).toStrictEqual(["a", "a/bar.txt"]);
       }
       done();
     });
@@ -105,7 +105,7 @@ describe("fs.globSync", () => {
 
   it.each([
     ["*.txt", ["foo.txt"]],
-    ["a/**", isWindows ? ["a\\bar.txt", "a\\baz.js"] : ["a/bar.txt", "a/baz.js"]],
+    ["a/**", isWindows ? ["a", "a\\bar.txt", "a\\baz.js"] : ["a", "a/bar.txt", "a/baz.js"]],
   ])("fs.glob(%p, { cwd: /tmp/fs-glob }) === %p", (pattern, expected) => {
     expect(fs.globSync(pattern, { cwd: tmp }).sort()).toStrictEqual(expected);
   });
@@ -127,8 +127,8 @@ describe("fs.globSync", () => {
 
   it("can filter out files", () => {
     const exclude = (path: string) => path.endsWith(".js");
-    const expected = isWindows ? ["a\\bar.txt"] : ["a/bar.txt"];
-    expect(fs.globSync("a/*", { cwd: tmp, exclude })).toStrictEqual(expected);
+    const expected = isWindows ? ["a", "a\\bar.txt"] : ["a", "a/bar.txt"];
+    expect(fs.globSync("a/**", { cwd: tmp, exclude }).sort()).toStrictEqual(expected);
   });
   it("can filter out files (2)", () => {
     const exclude = ["**/*.js"];
@@ -211,8 +211,9 @@ describe("fs.promises.glob", () => {
 
   it("can filter out files", async () => {
     const exclude = (path: string) => path.endsWith(".js");
-    const expected = isWindows ? ["a\\bar.txt"] : ["a/bar.txt"];
-    expect(Array.fromAsync(fs.promises.glob("a/*", { cwd: tmp, exclude }))).resolves.toStrictEqual(expected);
+    const expected = isWindows ? ["a", "a\\bar.txt"] : ["a", "a/bar.txt"];
+    const paths = await Array.fromAsync(fs.promises.glob("a/**", { cwd: tmp, exclude }));
+    expect(paths.sort()).toStrictEqual(expected);
   });
 
   it("can filter out files (2)", async () => {
@@ -232,3 +233,31 @@ describe("fs.promises.glob", () => {
     expect(Array.fromAsync(fs.promises.glob(["a/bar.txt", "a/baz.js"], { cwd: tmp }))).resolves.toStrictEqual(expected);
   });
 }); // 
+
+describe("fs.globSync exclude with withFileTypes", () => {
+  it("invokes the exclude callback with Dirents when cwd differs from process.cwd()", () => {
+    const dir = tempDirWithFiles("glob-exclude-dirent", {
+      "skip/inner.txt": "x",
+      "keep/inner.txt": "y",
+    });
+    // The Dirents handed to exclude must be stat'ed relative to options.cwd,
+    // not process.cwd() (a relative lookup would silently skip the callback).
+    const seen: string[] = [];
+    const results = fs.globSync("**", {
+      cwd: dir,
+      withFileTypes: true,
+      exclude: (dirent: any) => {
+        seen.push(dirent.name);
+        return dirent.name === "skip";
+      },
+    }) as any[];
+    expect(seen).toContain("skip");
+    const names = results.map(d => d.name);
+    expect(names).toContain("keep");
+    expect(names).not.toContain("skip");
+    // skip/inner.txt pruned with its directory; keep/inner.txt survives
+    const inners = results.filter(d => d.name === "inner.txt");
+    expect(inners).toHaveLength(1);
+    expect(String(inners[0].parentPath).replaceAll("\\", "/")).toEndWith("keep");
+  });
+});
diff --git a/test/js/node/fs/promises.test.js b/test/js/node/fs/promises.test.js
index f82fd064f93c..4e01b7ca1599 100644
--- a/test/js/node/fs/promises.test.js
+++ b/test/js/node/fs/promises.test.js
@@ -288,3 +288,117 @@ test("fs.promises async stack with Promise.all", async () => {
   // Promise.all uses combinator context — must not crash.
   expect(typeof caught.stack === "string" || caught.stack === undefined).toBe(true);
 });
+
+it("an unused FileHandle.writer() does not prevent close()", async () => {
+  const dir = tempDirWithFiles("unused-writer", { "x.txt": "hello" });
+  const fh = await fsPromises.open(join(dir, "x.txt"), "r+");
+  fh.writer(); // never written to, never ended
+  // must not hang: the writer only refs the handle once a write happens
+  await fh.close();
+  expect(fh.fd).toBe(-1);
+});
+
+it("sources created before close() refuse to use the stale fd", async () => {
+  const dir = tempDirWithFiles("stale-fd", { "x.txt": "hello" });
+  const file = join(dir, "x.txt");
+
+  // writer
+  {
+    const fh = await fsPromises.open(file, "r+");
+    const w = fh.writer();
+    await fh.close();
+    await expect(w.write(Buffer.from("a"))).rejects.toMatchObject({ code: "ERR_INVALID_STATE" });
+    expect(() => w.writeSync(Buffer.from("a"))).toThrow(expect.objectContaining({ code: "ERR_INVALID_STATE" }));
+  }
+  // pull
+  {
+    const fh = await fsPromises.open(file, "r");
+    const src = fh.pull();
+    await fh.close();
+    await expect(
+      (async () => {
+        for await (const _ of src);
+      })(),
+    ).rejects.toMatchObject({ code: "ERR_INVALID_STATE" });
+  }
+  // pullSync
+  {
+    const fh = await fsPromises.open(file, "r");
+    const src = fh.pullSync();
+    await fh.close();
+    expect(() => {
+      for (const _ of src);
+    }).toThrow(expect.objectContaining({ code: "ERR_INVALID_STATE" }));
+  }
+});
+
+it("rm and promises.rm report ERR_FS_EISDIR for directories like rmSync", async () => {
+  const dir = tempDirWithFiles("rm-eisdir", { "sub/a.txt": "x" });
+  const target = join(dir, "sub");
+  await expect(fsPromises.rm(target)).rejects.toMatchObject({ code: "ERR_FS_EISDIR" });
+  const { promise, resolve } = Promise.withResolvers();
+  fs.rm(target, err => resolve(err));
+  expect((await promise)?.code).toBe("ERR_FS_EISDIR");
+  // directory is still removable the supported way
+  await fsPromises.rm(target, { recursive: true });
+  expect(fs.existsSync(target)).toBe(false);
+});
+
+it("close() while an operation is in flight actually closes the fd", async () => {
+  const dir = tempDirWithFiles("deferred-close", { "x.txt": "hello" });
+  const fh = await fsPromises.open(join(dir, "x.txt"), "r");
+  const fd = fh.fd;
+  // take an extra ref so close() defers, then release it
+  const read = fh.read(Buffer.alloc(5), 0, 5, 0);
+  const closed = fh.close();
+  await read;
+  await closed;
+  expect(fh.fd).toBe(-1);
+  // the deferred path must have issued the real close; nothing else runs in
+  // this process between the close and this check, so EBADF is deterministic
+  expect(() => fs.fstatSync(fd)).toThrow(expect.objectContaining({ code: "EBADF" }));
+});
+
+it("fail()/end() with autoClose defer the close past an in-flight write", async () => {
+  const dir = tempDirWithFiles("writer-teardown", { "a.bin": "", "b.bin": "" });
+  // fail() while a large write is on the threadpool must not close the fd
+  // under it; the write completes, then the handle closes.
+  {
+    const fh = await fsPromises.open(join(dir, "a.bin"), "w");
+    const w = fh.writer({ autoClose: true });
+    const big = Buffer.alloc(8 << 20, 65);
+    const pending = w.write(big);
+    w.fail(new Error("stop"));
+    await pending; // must not reject with EBADF
+    expect(fs.statSync(join(dir, "a.bin")).size).toBe(big.byteLength);
+    expect(fh.fd).toBe(-1); // deferred teardown closed the handle
+  }
+  // end() while a write is pending waits for it and reports all bytes
+  {
+    const fh = await fsPromises.open(join(dir, "b.bin"), "w");
+    const w = fh.writer({ autoClose: true });
+    const big = Buffer.alloc(8 << 20, 66);
+    const pending = w.write(big);
+    const total = await w.end();
+    await pending;
+    expect(total).toBe(big.byteLength);
+    expect(fs.statSync(join(dir, "b.bin")).size).toBe(big.byteLength);
+    expect(fh.fd).toBe(-1);
+  }
+});
+
+it("teardown waits for every concurrent in-flight write", async () => {
+  const dir = tempDirWithFiles("writer-concurrent", { "a.bin": "" });
+  const fh = await fsPromises.open(join(dir, "a.bin"), "w");
+  const w = fh.writer({ autoClose: true, start: 0 });
+  const big = Buffer.alloc(4 << 20, 65);
+  // two unawaited writes in flight; the first one finishing must not run the
+  // deferred teardown while the second is still on the threadpool
+  const p1 = w.write(big);
+  const p2 = w.write(big);
+  w.fail(new Error("stop"));
+  await p1;
+  await p2; // must not reject with EBADF
+  expect(fs.statSync(join(dir, "a.bin")).size).toBe(big.byteLength * 2);
+  expect(fh.fd).toBe(-1);
+});
diff --git a/test/js/node/test/common/fs.js b/test/js/node/test/common/fs.js
new file mode 100644
index 000000000000..fdf7cbece5ce
--- /dev/null
+++ b/test/js/node/test/common/fs.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const { mustNotMutateObjectDeep } = require('.');
+const { readdirSync } = require('node:fs');
+const { join } = require('node:path');
+const assert = require('node:assert');
+const tmpdir = require('./tmpdir.js');
+
+let dirc = 0;
+function nextdir(dirname) {
+  return tmpdir.resolve(dirname || `copy_%${++dirc}`);
+}
+
+function assertDirEquivalent(dir1, dir2) {
+  const dir1Entries = [];
+  collectEntries(dir1, dir1Entries);
+  const dir2Entries = [];
+  collectEntries(dir2, dir2Entries);
+  assert.strictEqual(dir1Entries.length, dir2Entries.length);
+  for (const entry1 of dir1Entries) {
+    const entry2 = dir2Entries.find((entry) => {
+      return entry.name === entry1.name;
+    });
+    assert(entry2, `entry ${entry2.name} not copied`);
+    if (entry1.isFile()) {
+      assert(entry2.isFile(), `${entry2.name} was not file`);
+    } else if (entry1.isDirectory()) {
+      assert(entry2.isDirectory(), `${entry2.name} was not directory`);
+    } else if (entry1.isSymbolicLink()) {
+      assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`);
+    }
+  }
+}
+
+function collectEntries(dir, dirEntries) {
+  const newEntries = readdirSync(dir, mustNotMutateObjectDeep({ withFileTypes: true }));
+  for (const entry of newEntries) {
+    if (entry.isDirectory()) {
+      collectEntries(join(dir, entry.name), dirEntries);
+    }
+  }
+  dirEntries.push(...newEntries);
+}
+
+module.exports = {
+  nextdir,
+  assertDirEquivalent,
+  collectEntries,
+};
diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js
index ae6c57a1c777..eb332f3704d0 100644
--- a/test/js/node/test/common/index.js
+++ b/test/js/node/test/common/index.js
@@ -1150,6 +1150,15 @@ const common = {
     return hasOpenSSL(3, 1);
   },
 
+  get isInsideDirWithUnusualChars() {
+    return __dirname.includes('%') ||
+           (!isWindows && __dirname.includes('\\')) ||
+           __dirname.includes('$') ||
+           __dirname.includes('\n') ||
+           __dirname.includes('\r') ||
+           __dirname.includes('\t');
+  },
+
   get hasOpenSSL32() {
     return hasOpenSSL(3, 2);
   },
diff --git a/test/js/node/test/common/index.mjs b/test/js/node/test/common/index.mjs
index 748977b85f80..898e6f2d2e80 100644
--- a/test/js/node/test/common/index.mjs
+++ b/test/js/node/test/common/index.mjs
@@ -33,6 +33,7 @@ const {
   isMacOS,
   isSunOS,
   isWindows,
+  isInsideDirWithUnusualChars,
   localIPv6Hosts,
   mustCall,
   mustCallAtLeast,
@@ -88,6 +89,7 @@ export {
   isMacOS,
   isSunOS,
   isWindows,
+  isInsideDirWithUnusualChars,
   localIPv6Hosts,
   mustCall,
   mustCallAtLeast,
diff --git a/test/js/node/test/common/watch.js b/test/js/node/test/common/watch.js
new file mode 100644
index 000000000000..c3d22c30f78e
--- /dev/null
+++ b/test/js/node/test/common/watch.js
@@ -0,0 +1,240 @@
+'use strict';
+const common = require('./index.js');
+const tmpdir = require('./tmpdir.js');
+const fixtures = require('./fixtures.js');
+const { writeFileSync, readdirSync, readFileSync, renameSync, unlinkSync } = require('node:fs');
+const { spawn } = require('node:child_process');
+const { once } = require('node:events');
+const assert = require('node:assert');
+const { setTimeout } = require('node:timers/promises');
+
+function skipIfNoWatch() {
+  if (common.isIBMi) {
+    common.skip('IBMi does not support `fs.watch()`');
+  }
+
+  if (common.isAIX) {
+    common.skip('folder watch capability is limited in AIX.');
+  }
+}
+
+function skipIfNoWatchModeSignals() {
+  if (common.isWindows) {
+    common.skip('no signals on Windows');
+  }
+
+  if (common.isIBMi) {
+    common.skip('IBMi does not support `fs.watch()`');
+  }
+
+  if (common.isAIX) {
+    common.skip('folder watch capability is limited in AIX.');
+  }
+}
+
+const fixturePaths = {};
+const fixtureContent = {};
+
+function refreshForTestRunnerWatch() {
+  tmpdir.refresh();
+  const files = readdirSync(fixtures.path('test-runner-watch'));
+  for (const file of files) {
+    const src = fixtures.path('test-runner-watch', file);
+    const dest = tmpdir.resolve(file);
+    fixturePaths[file] = dest;
+    fixtureContent[file] = readFileSync(src, 'utf8');
+    writeFileSync(dest, fixtureContent[file]);
+  }
+}
+
+async function performFileOperation(operation, useRunApi, timeout = 1000) {
+  if (useRunApi) {
+    const interval = setInterval(() => {
+      operation();
+      clearInterval(interval);
+    }, common.platformTimeout(timeout));
+  } else {
+    operation();
+    await setTimeout(common.platformTimeout(timeout));
+  }
+}
+
+function assertTestOutput(run, shouldCheckRecursion = false) {
+  if (shouldCheckRecursion) {
+    assert.doesNotMatch(run, /run\(\) is being called recursively/);
+  }
+  assert.match(run, /tests 1/);
+  assert.match(run, /pass 1/);
+  assert.match(run, /fail 0/);
+  assert.match(run, /cancelled 0/);
+}
+
+async function testRunnerWatch({
+  fileToUpdate,
+  file,
+  action = 'update',
+  fileToCreate,
+  isolation,
+  useRunApi = false,
+  cwd = tmpdir.path,
+  runnerCwd,
+}) {
+  const ran1 = Promise.withResolvers();
+  const ran2 = Promise.withResolvers();
+
+  let args;
+  if (useRunApi) {
+    // Use the fixture that calls run() API
+    const runner = fixtures.path('test-runner-watch.mjs');
+    args = [runner];
+    if (file) args.push('--file', file);
+    if (runnerCwd) args.push('--cwd', runnerCwd);
+    if (isolation) args.push('--isolation', isolation);
+  } else {
+    // Use CLI --watch --test flags
+    args = ['--watch', '--test', '--test-reporter=spec',
+            isolation ? `--test-isolation=${isolation}` : '',
+            file ? fixturePaths[file] : undefined].filter(Boolean);
+  }
+
+  const child = spawn(process.execPath, args,
+                      { encoding: 'utf8', stdio: 'pipe', cwd });
+  let stdout = '';
+  let currentRun = '';
+  const runs = [];
+
+  child.stdout.on('data', (data) => {
+    stdout += data.toString();
+    currentRun += data.toString();
+    const testRuns = stdout.match(/^\S+ duration_ms\s\d+/gm);
+    if (testRuns?.length >= 1) ran1.resolve();
+    if (testRuns?.length >= 2) ran2.resolve();
+  });
+
+  const testUpdate = async () => {
+    await ran1.promise;
+    runs.push(currentRun);
+    currentRun = '';
+    const content = fixtureContent[fileToUpdate];
+    const path = fixturePaths[fileToUpdate];
+
+    await performFileOperation(
+      () => writeFileSync(path, content),
+      useRunApi,
+    );
+    await ran2.promise;
+
+    runs.push(currentRun);
+    child.kill();
+    await once(child, 'exit');
+
+    assert.strictEqual(runs.length, 2);
+
+    for (const run of runs) {
+      assertTestOutput(run, useRunApi);
+    }
+  };
+
+  const testRename = async () => {
+    await ran1.promise;
+    runs.push(currentRun);
+    currentRun = '';
+    const fileToRenamePath = tmpdir.resolve(fileToUpdate);
+    const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
+
+    await performFileOperation(
+      () => renameSync(fileToRenamePath, newFileNamePath),
+      useRunApi,
+    );
+    await ran2.promise;
+
+    runs.push(currentRun);
+    child.kill();
+    await once(child, 'exit');
+
+    assert.strictEqual(runs.length, 2);
+
+    const [firstRun, secondRun] = runs;
+    assertTestOutput(firstRun, useRunApi);
+
+    if (action === 'rename2') {
+      assert.match(secondRun, /MODULE_NOT_FOUND/);
+      return;
+    }
+
+    assertTestOutput(secondRun, useRunApi);
+  };
+
+  const testDelete = async () => {
+    await ran1.promise;
+    runs.push(currentRun);
+    currentRun = '';
+    const fileToDeletePath = tmpdir.resolve(fileToUpdate);
+
+    if (useRunApi) {
+      const { existsSync } = require('node:fs');
+      const interval = setInterval(() => {
+        if (existsSync(fileToDeletePath)) {
+          unlinkSync(fileToDeletePath);
+        } else {
+          ran2.resolve();
+          clearInterval(interval);
+        }
+      }, common.platformTimeout(1000));
+      await ran2.promise;
+    } else {
+      unlinkSync(fileToDeletePath);
+      await setTimeout(common.platformTimeout(2000));
+      ran2.resolve();
+    }
+
+    runs.push(currentRun);
+    child.kill();
+    await once(child, 'exit');
+
+    assert.strictEqual(runs.length, 2);
+
+    for (const run of runs) {
+      assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
+    }
+  };
+
+  const testCreate = async () => {
+    await ran1.promise;
+    runs.push(currentRun);
+    currentRun = '';
+    const newFilePath = tmpdir.resolve(fileToCreate);
+
+    await performFileOperation(
+      () => writeFileSync(newFilePath, 'module.exports = {};'),
+      useRunApi,
+    );
+    await ran2.promise;
+
+    runs.push(currentRun);
+    child.kill();
+    await once(child, 'exit');
+
+    for (const run of runs) {
+      assertTestOutput(run, false);
+    }
+  };
+
+  action === 'update' && await testUpdate();
+  action === 'rename' && await testRename();
+  action === 'rename2' && await testRename();
+  action === 'delete' && await testDelete();
+  action === 'create' && await testCreate();
+
+  return runs;
+}
+
+
+module.exports = {
+  skipIfNoWatch,
+  skipIfNoWatchModeSignals,
+  testRunnerWatch,
+  refreshForTestRunnerWatch,
+  fixtureContent,
+  fixturePaths,
+};
diff --git a/test/js/node/test/parallel/test-fs-append-file.js b/test/js/node/test/parallel/test-fs-append-file.js
index 1e20625e5b96..cdcc40b52feb 100644
--- a/test/js/node/test/parallel/test-fs-append-file.js
+++ b/test/js/node/test/parallel/test-fs-append-file.js
@@ -169,17 +169,16 @@ const throwNextTick = (e) => { process.nextTick(() => { throw e; }); };
 
   let fd;
   fs.promises.open(filename, 'a+')
-    .then(common.mustCall((fileDescriptor) => {
+    .then((fileDescriptor) => {
       fd = fileDescriptor;
       return fs.promises.appendFile(fd, s);
-    }))
-    .then(common.mustCall(() => fd.close()))
-    .then(common.mustCall(() => fs.promises.readFile(filename)))
+    })
+    .then(() => fd.close())
+    .then(() => fs.promises.readFile(filename))
     .then(common.mustCall((buffer) => {
       assert.strictEqual(Buffer.byteLength(s) + currentFileData.length,
                          buffer.length);
-    }))
-    .catch(throwNextTick);
+    }));
 }
 
 assert.throws(
diff --git a/test/js/node/test/parallel/test-fs-chown-negative-one.js b/test/js/node/test/parallel/test-fs-chown-negative-one.js
new file mode 100644
index 000000000000..092d4782d115
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-chown-negative-one.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFile = path.join(tmpdir.path, 'chown-test-file.txt');
+
+fs.writeFileSync(testFile, 'test content for chown');
+
+const stats = fs.statSync(testFile);
+const uid = stats.uid;
+const gid = stats.gid;
+
+// -1 for uid and gid means "don't change the value"
+{
+  fs.chown(testFile, -1, -1, common.mustSucceed(() => {
+    const stats = fs.statSync(testFile);
+    assert.strictEqual(stats.uid, uid);
+    assert.strictEqual(stats.gid, gid);
+  }));
+}
+{
+  fs.chownSync(testFile, -1, -1);
+  const stats = fs.statSync(testFile);
+  assert.strictEqual(stats.uid, uid);
+  assert.strictEqual(stats.gid, gid);
+}
diff --git a/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js b/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js
index d668ec63ec9a..f71e8a22d1fd 100644
--- a/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js
+++ b/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js
@@ -27,12 +27,12 @@ function beforeEach() {
   fs.writeFileSync(dest, 'dest');
   fs.chmodSync(dest, '444');
 
-  const check = (err) => {
+  const check = common.mustCall((err) => {
     const expected = ['EACCES', 'EPERM'];
     assert(expected.includes(err.code), `${err.code} not in ${expected}`);
     assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'dest');
     return true;
-  };
+  });
 
   return { source, dest, check };
 }
diff --git a/test/js/node/test/parallel/test-fs-cp-async-async-filter-function.mjs b/test/js/node/test/parallel/test-fs-cp-async-async-filter-function.mjs
new file mode 100644
index 000000000000..91b17a0f8823
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-async-filter-function.mjs
@@ -0,0 +1,32 @@
+// This tests that cp() supports async filter function.
+
+import { mustCall } from '../common/index.mjs';
+import { nextdir, collectEntries } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, statSync } from 'node:fs';
+import { setTimeout } from 'node:timers/promises';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, {
+  filter: async (path) => {
+    await setTimeout(5, 'done');
+    const pathStat = statSync(path);
+    return pathStat.isDirectory() || path.endsWith('.js');
+  },
+  dereference: true,
+  recursive: true,
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  const destEntries = [];
+  collectEntries(dest, destEntries);
+  for (const entry of destEntries) {
+    assert.strictEqual(
+      entry.isDirectory() || entry.name.endsWith('.js'),
+      true
+    );
+  }
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs b/test/js/node/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs
new file mode 100644
index 000000000000..a5064a9a71a2
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs
@@ -0,0 +1,22 @@
+// This tests that cp() copies link if it does not point to folder in src.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync, readlinkSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, join(src, 'a', 'c'));
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(dest, join(dest, 'a', 'c'));
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err, null);
+  const link = readlinkSync(join(dest, 'a', 'c'));
+  assert.strictEqual(link, src);
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs b/test/js/node/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs
new file mode 100644
index 000000000000..0c92b3eb6276
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs
@@ -0,0 +1,25 @@
+// This tests that it does not fail if the same directory is copied to dest
+// twice, when dereference is true, and force is false (fails silently).
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, cpSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import { nextdir } from '../common/fs.js';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+const destFile = join(dest, 'a/b/README2.md');
+cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+cp(src, dest, {
+  dereference: true,
+  recursive: true
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  const stat = lstatSync(destFile);
+  assert(stat.isFile());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-dereference-symlink.mjs b/test/js/node/test/parallel/test-fs-cp-async-dereference-symlink.mjs
new file mode 100644
index 000000000000..7edc8b8c66bb
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-dereference-symlink.mjs
@@ -0,0 +1,27 @@
+// This tests that cp() copies file itself, rather than symlink, when dereference is true.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, lstatSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync(join(src, 'foo.js'), join(src, 'bar.js'));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+const destFile = join(dest, 'foo.js');
+
+cp(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true }),
+   mustCall((err) => {
+     assert.strictEqual(err, null);
+     const stat = lstatSync(destFile);
+     assert(stat.isFile());
+   })
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs b/test/js/node/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs
new file mode 100644
index 000000000000..da8d606963bf
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs
@@ -0,0 +1,21 @@
+// This tests that cp() returns error if parent directory of symlink in dest points to src.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+// Create symlink in dest pointing to src.
+const destLink = join(dest, 'b');
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, destLink);
+cp(src, join(dest, 'b', 'c'), mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-dir-exists-error-on-exist.mjs b/test/js/node/test/parallel/test-fs-cp-async-dir-exists-error-on-exist.mjs
new file mode 100644
index 000000000000..90a7d19c2fc1
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-dir-exists-error-on-exist.mjs
@@ -0,0 +1,30 @@
+// This tests that cp() returns error if errorOnExist is true, force is false,
+// and the destination directory already exists (even if contents don't conflict).
+
+import { mustCall } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync, writeFileSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+const dest = nextdir();
+
+// Create source directory with a file
+mkdirSync(src);
+writeFileSync(`${src}/file.txt`, 'test');
+
+// Create destination directory with different file
+mkdirSync(dest);
+writeFileSync(`${dest}/other.txt`, 'existing');
+
+// Should fail because dest directory already exists
+cp(src, dest, {
+  recursive: true,
+  errorOnExist: true,
+  force: false,
+}, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-dir-to-file.mjs b/test/js/node/test/parallel/test-fs-cp-async-dir-to-file.mjs
new file mode 100644
index 000000000000..16d9ce6f0c23
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-dir-to-file.mjs
@@ -0,0 +1,17 @@
+// This tests that cp() returns error if attempt is made to copy directory to file.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+const dest = fixtures.path('copy/kitchen-sink/README.md');
+cp(src, dest, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-error-on-exist.mjs b/test/js/node/test/parallel/test-fs-cp-async-error-on-exist.mjs
new file mode 100644
index 000000000000..c6df45db7f31
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-error-on-exist.mjs
@@ -0,0 +1,22 @@
+// This tests that cp() returns error if errorOnExist is true, force is false, and file or folder copied over.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+cp(src, dest, {
+  dereference: true,
+  errorOnExist: true,
+  force: false,
+  recursive: true,
+}, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-file-to-dir.mjs b/test/js/node/test/parallel/test-fs-cp-async-file-to-dir.mjs
new file mode 100644
index 000000000000..94dc193f5c52
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-file-to-dir.mjs
@@ -0,0 +1,17 @@
+// This tests that cp() returns error if attempt is made to copy file to directory.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink/README.md');
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+cp(src, dest, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-file-to-file.mjs b/test/js/node/test/parallel/test-fs-cp-async-file-to-file.mjs
new file mode 100644
index 000000000000..faff91e6a2bb
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-file-to-file.mjs
@@ -0,0 +1,19 @@
+// This tests that cp() allows file to be copied to a file path.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const srcFile = fixtures.path('copy/kitchen-sink/README.md');
+const destFile = join(nextdir(), 'index.js');
+cp(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true }), mustCall((err) => {
+  assert.strictEqual(err, null);
+  const stat = lstatSync(destFile);
+  assert(stat.isFile());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-file-url.mjs b/test/js/node/test/parallel/test-fs-cp-async-file-url.mjs
new file mode 100644
index 000000000000..4c1ea1fdd56b
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-file-url.mjs
@@ -0,0 +1,18 @@
+// This tests that it accepts file URL as src and dest.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import { pathToFileURL } from 'node:url';
+import tmpdir from '../common/tmpdir.js';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+
+tmpdir.refresh();
+
+const src = './test/fixtures/copy/kitchen-sink';
+const dest = nextdir();
+cp(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true }),
+   mustCall((err) => {
+     assert.strictEqual(err, null);
+     assertDirEquivalent(src, dest);
+   }));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-filter-child-folder.mjs b/test/js/node/test/parallel/test-fs-cp-async-filter-child-folder.mjs
new file mode 100644
index 000000000000..b1ebeca514e4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-filter-child-folder.mjs
@@ -0,0 +1,26 @@
+// This tests that cp() should not throw exception if child folder is filtered out.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, cpSync, mkdirSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'test-cp'), mustNotMutateObjectDeep({ recursive: true }));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(dest, 'test-cp'), 'test-content', mustNotMutateObjectDeep({ mode: 0o444 }));
+
+const opts = {
+  filter: (path) => !path.includes('test-cp'),
+  recursive: true,
+};
+cp(src, dest, opts, mustCall((err) => {
+  assert.strictEqual(err, null);
+}));
+cpSync(src, dest, opts);
diff --git a/test/js/node/test/parallel/test-fs-cp-async-filter-function.mjs b/test/js/node/test/parallel/test-fs-cp-async-filter-function.mjs
new file mode 100644
index 000000000000..bb8145b035b8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-filter-function.mjs
@@ -0,0 +1,31 @@
+// This tests that cp() applies filter function.
+
+import { mustCall } from '../common/index.mjs';
+import { nextdir, collectEntries } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, statSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, {
+  filter: (path) => {
+    const pathStat = statSync(path);
+    return pathStat.isDirectory() || path.endsWith('.js');
+  },
+  dereference: true,
+  recursive: true,
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  const destEntries = [];
+  collectEntries(dest, destEntries);
+  for (const entry of destEntries) {
+    assert.strictEqual(
+      entry.isDirectory() || entry.name.endsWith('.js'),
+      true
+    );
+  }
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-identical-src-dest.mjs b/test/js/node/test/parallel/test-fs-cp-async-identical-src-dest.mjs
new file mode 100644
index 000000000000..20bed5f92685
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-identical-src-dest.mjs
@@ -0,0 +1,14 @@
+// This tests that cp() returns error when src and dest are identical.
+
+import { mustCall } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+cp(src, src, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-invalid-mode-range.mjs b/test/js/node/test/parallel/test-fs-cp-async-invalid-mode-range.mjs
new file mode 100644
index 000000000000..12be6a6fadd2
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-invalid-mode-range.mjs
@@ -0,0 +1,13 @@
+// This tests that cp() throws if mode is out of range.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+assert.throws(
+  () => cp('a', 'b', { mode: -1 }, () => {}),
+  { code: 'ERR_OUT_OF_RANGE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-async-invalid-options-type.mjs b/test/js/node/test/parallel/test-fs-cp-async-invalid-options-type.mjs
new file mode 100644
index 000000000000..7c84d5f68013
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-invalid-options-type.mjs
@@ -0,0 +1,13 @@
+// This tests that cp() throws if options is not object.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+assert.throws(
+  () => cp('a', 'b', 'hello', () => {}),
+  { code: 'ERR_INVALID_ARG_TYPE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-async-nested-files-folders.mjs b/test/js/node/test/parallel/test-fs-cp-async-nested-files-folders.mjs
new file mode 100644
index 000000000000..9cc2115bc1fc
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-nested-files-folders.mjs
@@ -0,0 +1,17 @@
+// This tests that cp() copies a nested folder structure with files and folders.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err, null);
+  assertDirEquivalent(src, dest);
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-no-errors-force-false.mjs b/test/js/node/test/parallel/test-fs-cp-async-no-errors-force-false.mjs
new file mode 100644
index 000000000000..609706a8306f
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-no-errors-force-false.mjs
@@ -0,0 +1,28 @@
+// This tests that it does not throw errors when directory is copied over and force is false.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, cpSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'README.md'), 'hello world', 'utf8');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+const initialStat = lstatSync(join(dest, 'README.md'));
+cp(src, dest, {
+  dereference: true,
+  force: false,
+  recursive: true,
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  assertDirEquivalent(src, dest);
+  // File should not have been copied over, so access times will be identical:
+  const finalStat = lstatSync(join(dest, 'README.md'));
+  assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-no-recursive.mjs b/test/js/node/test/parallel/test-fs-cp-async-no-recursive.mjs
new file mode 100644
index 000000000000..fd58ee81d58e
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-no-recursive.mjs
@@ -0,0 +1,16 @@
+// This tests that cp() returns error if directory copied without recursive flag.
+
+import { mustCall } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_EISDIR');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-overwrites-force-true.mjs b/test/js/node/test/parallel/test-fs-cp-async-overwrites-force-true.mjs
new file mode 100644
index 000000000000..1d5006256a7a
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-overwrites-force-true.mjs
@@ -0,0 +1,23 @@
+// This tests that it overwrites existing files if force is true.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8');
+
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err, null);
+  assertDirEquivalent(src, dest);
+  const content = readFileSync(join(dest, 'README.md'), 'utf8');
+  assert.strictEqual(content.trim(), '# Hello');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs b/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs
new file mode 100644
index 000000000000..fff2e199fcd0
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs
@@ -0,0 +1,26 @@
+// This tests that it makes file writeable when updating timestamp, if not writeable.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 }));
+cp(src, dest, {
+  preserveTimestamps: true,
+  recursive: true,
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  assertDirEquivalent(src, dest);
+  const srcStat = lstatSync(join(src, 'foo.txt'));
+  const destStat = lstatSync(join(dest, 'foo.txt'));
+  assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps.mjs b/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps.mjs
new file mode 100644
index 000000000000..8e55f07ca602
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-preserve-timestamps.mjs
@@ -0,0 +1,24 @@
+// This tests that cp() copies timestamps from src to dest if preserveTimestamps is true.
+
+import { mustCall } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, {
+  preserveTimestamps: true,
+  recursive: true
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  assertDirEquivalent(src, dest);
+  const srcStat = lstatSync(join(src, 'index.js'));
+  const destStat = lstatSync(join(dest, 'index.js'));
+  assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-same-dir-twice.mjs b/test/js/node/test/parallel/test-fs-cp-async-same-dir-twice.mjs
new file mode 100644
index 000000000000..0c92b3eb6276
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-same-dir-twice.mjs
@@ -0,0 +1,25 @@
+// This tests that it does not fail if the same directory is copied to dest
+// twice, when dereference is true, and force is false (fails silently).
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, cpSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import { nextdir } from '../common/fs.js';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+const destFile = join(dest, 'a/b/README2.md');
+cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+cp(src, dest, {
+  dereference: true,
+  recursive: true
+}, mustCall((err) => {
+  assert.strictEqual(err, null);
+  const stat = lstatSync(destFile);
+  assert(stat.isFile());
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs b/test/js/node/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs
new file mode 100644
index 000000000000..4778bc233191
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs
@@ -0,0 +1,29 @@
+// This tests that cp() should not throw exception if dest is invalid but filtered out.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, cpSync, mkdirSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+// Create dest as a file.
+// Expect: cp skips the copy logic entirely and won't throw any exception in path validation process.
+const src = join(nextdir(), 'bar');
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+
+const destParent = nextdir();
+const dest = join(destParent, 'bar');
+mkdirSync(destParent, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(dest, 'test-content', mustNotMutateObjectDeep({ mode: 0o444 }));
+
+const opts = {
+  filter: (path) => !path.includes('bar'),
+  recursive: true,
+};
+cp(src, dest, opts, mustCall((err) => {
+  assert.strictEqual(err, null);
+}));
+cpSync(src, dest, opts);
diff --git a/test/js/node/test/parallel/test-fs-cp-async-socket.mjs b/test/js/node/test/parallel/test-fs-cp-async-socket.mjs
new file mode 100644
index 000000000000..4ebc56cc3f66
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-socket.mjs
@@ -0,0 +1,34 @@
+// This tests that cp() returns an error if attempt is made to copy socket.
+
+import * as common from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, mkdirSync } from 'node:fs';
+import { createServer } from 'node:net';
+import { join } from 'node:path';
+import { nextdir } from '../common/fs.js';
+import tmpdir from '../common/tmpdir.js';
+
+const isWindows = process.platform === 'win32';
+if (isWindows) {
+  common.skip('No socket support on Windows');
+}
+
+// See https://github.com/nodejs/node/pull/48409
+if (common.isInsideDirWithUnusualChars) {
+  common.skip('Test is borken in directories with unusual characters');
+}
+
+tmpdir.refresh();
+
+{
+  const src = nextdir();
+  mkdirSync(src);
+  const dest = nextdir();
+  const sock = join(src, `${process.pid}.sock`);
+  const server = createServer();
+  server.listen(sock);
+  cp(sock, dest, common.mustCall((err) => {
+    assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET');
+    server.close();
+  }));
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs b/test/js/node/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs
new file mode 100644
index 000000000000..6884b2ddcd69
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs
@@ -0,0 +1,12 @@
+// This tests that cp() returns error if attempt is made to copy to subdirectory of self.
+
+import { mustCall } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp } from 'node:fs';
+import fixtures from '../common/fixtures.js';
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = fixtures.path('copy/kitchen-sink/a');
+cp(src, dest, mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs b/test/js/node/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs
new file mode 100644
index 000000000000..dab9c572f9d7
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs
@@ -0,0 +1,21 @@
+// This tests that cp() returns error if symlink in dest points to location in src.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, join(dest, 'a', 'c'));
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-symlink-over-file.mjs b/test/js/node/test/parallel/test-fs-cp-async-symlink-over-file.mjs
new file mode 100644
index 000000000000..8685a3d611dc
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-symlink-over-file.mjs
@@ -0,0 +1,21 @@
+// This tests that cp() returns EEXIST error if attempt is made to copy symlink over file.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8');
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err.code, 'EEXIST');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs b/test/js/node/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs
new file mode 100644
index 000000000000..f8e60fe1fa2b
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs
@@ -0,0 +1,20 @@
+// This tests that cp() returns error if symlink in src points to location in dest.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cp, cpSync, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+mkdirSync(dest);
+symlinkSync(dest, join(src, 'link'));
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
+  assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-async-with-mode-flags.mjs b/test/js/node/test/parallel/test-fs-cp-async-with-mode-flags.mjs
new file mode 100644
index 000000000000..99f6b5fe09d9
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-async-with-mode-flags.mjs
@@ -0,0 +1,31 @@
+// This tests that it copies a nested folder structure with mode flags.
+
+import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cp, constants } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cp(src, dest, mustNotMutateObjectDeep({
+  recursive: true,
+  mode: constants.COPYFILE_FICLONE_FORCE,
+}), mustCall((err) => {
+  if (!err) {
+    // If the platform support `COPYFILE_FICLONE_FORCE` operation,
+    // it should reach to here.
+    assert.strictEqual(err, null);
+    assertDirEquivalent(src, dest);
+    return;
+  }
+
+  // If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
+  // it should enter this path.
+  assert.strictEqual(err.syscall, 'copyfile');
+  assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
+    err.code === 'ENOSYS' || err.code === 'EXDEV');
+}));
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-async-error.mjs b/test/js/node/test/parallel/test-fs-cp-promises-async-error.mjs
new file mode 100644
index 000000000000..b7817cfd8073
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-async-error.mjs
@@ -0,0 +1,23 @@
+// This tests that fs.promises.cp() allows async error to be caught.
+
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import fs from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+await assert.rejects(
+  fs.promises.cp(src, dest, {
+    dereference: true,
+    errorOnExist: true,
+    force: false,
+    recursive: true,
+  }),
+  { code: 'ERR_FS_CP_EEXIST' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-file-url.mjs b/test/js/node/test/parallel/test-fs-cp-promises-file-url.mjs
new file mode 100644
index 000000000000..552e9c0b272f
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-file-url.mjs
@@ -0,0 +1,21 @@
+// This tests that fs.promises.cp() accepts file URL as src and dest.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { promises as fs } from 'node:fs';
+import { pathToFileURL } from 'node:url';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+const p = await fs.cp(
+  pathToFileURL(src),
+  pathToFileURL(dest),
+  { recursive: true }
+);
+assert.strictEqual(p, undefined);
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-invalid-mode.mjs b/test/js/node/test/parallel/test-fs-cp-promises-invalid-mode.mjs
new file mode 100644
index 000000000000..2e200e56afe0
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-invalid-mode.mjs
@@ -0,0 +1,15 @@
+// This tests that fs.promises.cp() rejects if options.mode is invalid.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import fs from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+await assert.rejects(
+  fs.promises.cp('a', 'b', {
+    mode: -1,
+  }),
+  { code: 'ERR_OUT_OF_RANGE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-mode-flags.mjs b/test/js/node/test/parallel/test-fs-cp-promises-mode-flags.mjs
new file mode 100644
index 000000000000..b5633ae5e7f4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-mode-flags.mjs
@@ -0,0 +1,36 @@
+// This tests that fs.promises.cp() copies a nested folder structure with mode flags.
+// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`.
+
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { promises as fs, constants } from 'node:fs';
+import { assertDirEquivalent, nextdir } from '../common/fs.js';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+let p = null;
+let successFiClone = false;
+try {
+  p = await fs.cp(src, dest, mustNotMutateObjectDeep({
+    recursive: true,
+    mode: constants.COPYFILE_FICLONE_FORCE,
+  }));
+  successFiClone = true;
+} catch (err) {
+  // If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
+  // it should enter this path.
+  assert.strictEqual(err.syscall, 'copyfile');
+  assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
+    err.code === 'ENOSYS' || err.code === 'EXDEV');
+}
+
+if (successFiClone) {
+  // If the platform support `COPYFILE_FICLONE_FORCE` operation,
+  // it should reach to here.
+  assert.strictEqual(p, undefined);
+  assertDirEquivalent(src, dest);
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs b/test/js/node/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs
new file mode 100644
index 000000000000..21cd1ba9d257
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs
@@ -0,0 +1,16 @@
+// This tests that fs.promises.cp() copies a nested folder structure with files and folders.
+
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import fs from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+const p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assert.strictEqual(p, undefined);
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-promises-options-validation.mjs b/test/js/node/test/parallel/test-fs-cp-promises-options-validation.mjs
new file mode 100644
index 000000000000..a2cfad4552e4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-promises-options-validation.mjs
@@ -0,0 +1,13 @@
+// This tests that fs.promises.cp() rejects if options is not object.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import fs from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+await assert.rejects(
+  fs.promises.cp('a', 'b', () => {}),
+  { code: 'ERR_INVALID_ARG_TYPE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-apply-filter-function.mjs b/test/js/node/test/parallel/test-fs-cp-sync-apply-filter-function.mjs
new file mode 100644
index 000000000000..02e7446c572e
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-apply-filter-function.mjs
@@ -0,0 +1,28 @@
+// This tests that cpSync applies filter function.
+import '../common/index.mjs';
+import { nextdir, collectEntries } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, statSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(src, dest, {
+  filter: (path) => {
+    const pathStat = statSync(path);
+    return pathStat.isDirectory() || path.endsWith('.js');
+  },
+  dereference: true,
+  recursive: true,
+});
+const destEntries = [];
+collectEntries(dest, destEntries);
+for (const entry of destEntries) {
+  assert.strictEqual(
+    entry.isDirectory() || entry.name.endsWith('.js'),
+    true
+  );
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-async-filter-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-async-filter-error.mjs
new file mode 100644
index 000000000000..0bb2e62e768b
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-async-filter-error.mjs
@@ -0,0 +1,24 @@
+// This tests that cpSync throws error if filter function is asynchronous.
+import '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, statSync } from 'node:fs';
+import { setTimeout } from 'node:timers/promises';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+assert.throws(() => {
+  cpSync(src, dest, {
+    filter: async (path) => {
+      await setTimeout(5, 'done');
+      const pathStat = statSync(path);
+      return pathStat.isDirectory() || path.endsWith('.js');
+    },
+    dereference: true,
+    recursive: true,
+  });
+}, { code: 'ERR_INVALID_RETURN_VALUE' });
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs
new file mode 100644
index 000000000000..04388f3166b8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs
@@ -0,0 +1,24 @@
+// This tests that cpSync throws error if attempt is made to copy directory to file.
+import { isInsideDirWithUnusualChars, mustNotMutateObjectDeep, skip } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+// See https://github.com/nodejs/node/pull/48409
+if (isInsideDirWithUnusualChars) {
+  skip('Test is borken in directories with unusual characters');
+}
+
+tmpdir.refresh();
+
+{
+  const src = nextdir('FIRST_DIRECTORY');
+  mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+  const dest = fixtures.path('copy/kitchen-sink/README.md');
+  assert.throws(
+    () => cpSync(src, dest),
+    { code: 'ERR_FS_CP_DIR_TO_NON_DIR', message: /non-directory .*README\.md with directory .*FIRST_DIRECTORY/ }
+  );
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs
new file mode 100644
index 000000000000..114089977974
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs
@@ -0,0 +1,16 @@
+// This tests that cpSync throws error if directory copied without recursive flag.
+import '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+assert.throws(
+  () => cpSync(src, dest),
+  { code: 'ERR_FS_EISDIR' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs
new file mode 100644
index 000000000000..96e1fa3b4e2a
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs
@@ -0,0 +1,22 @@
+// This tests that cpSync throws error if attempt is made to copy file to directory.
+import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+// See https://github.com/nodejs/node/pull/48409
+if (isInsideDirWithUnusualChars) {
+  skip('Test is borken in directories with unusual characters');
+}
+
+const src = fixtures.path('copy/kitchen-sink/README.md');
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+assert.throws(
+  () => cpSync(src, dest),
+  { code: 'ERR_FS_CP_NON_DIR_TO_DIR' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs
new file mode 100644
index 000000000000..fa7ad45d82ac
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs
@@ -0,0 +1,13 @@
+// This tests that cpSync allows file to be copied to a file path.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import fixtures from '../common/fixtures.js';
+
+const srcFile = fixtures.path('copy/kitchen-sink/index.js');
+const destFile = join(nextdir(), 'index.js');
+cpSync(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true }));
+const stat = lstatSync(destFile);
+assert(stat.isFile());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs
new file mode 100644
index 000000000000..81a06cc224ae
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-socket-error.mjs
@@ -0,0 +1,34 @@
+// This tests that cpSync throws an error if attempt is made to copy socket.
+import * as common from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync } from 'node:fs';
+import { createServer } from 'node:net';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+const isWindows = process.platform === 'win32';
+if (isWindows) {
+  common.skip('No socket support on Windows');
+}
+
+// See https://github.com/nodejs/node/pull/48409
+if (common.isInsideDirWithUnusualChars) {
+  common.skip('Test is borken in directories with unusual characters');
+}
+
+tmpdir.refresh();
+
+{
+  const src = nextdir();
+  mkdirSync(src);
+  const dest = nextdir();
+  const sock = join(src, `${process.pid}.sock`);
+  const server = createServer();
+  server.listen(sock);
+  assert.throws(
+    () => cpSync(sock, dest),
+    { code: 'ERR_FS_CP_SOCKET' }
+  );
+  server.close();
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs
new file mode 100644
index 000000000000..fa5ec6a7dbc8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs
@@ -0,0 +1,26 @@
+// This tests that cpSync copies link if it does not point to folder in src.
+import { mustNotMutateObjectDeep, isWindows } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, symlinkSync, readlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, join(src, 'a', 'c'));
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(dest, join(dest, 'a', 'c'));
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+const link = readlinkSync(join(dest, 'a', 'c'));
+
+if (isWindows) {
+  // On Windows, readlinkSync() may return a path with uppercase drive letter,
+  // but paths are case-insensitive.
+  assert.strictEqual(link.toLowerCase(), src.toLowerCase());
+} else {
+  assert.strictEqual(link, src);
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs
new file mode 100644
index 000000000000..cf8f055bc242
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs
@@ -0,0 +1,21 @@
+// This tests that cpSync throws EEXIST error if attempt is made to copy symlink over file.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8');
+assert.throws(
+  () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })),
+  { code: 'EEXIST' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs
new file mode 100644
index 000000000000..a84dc07873be
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs
@@ -0,0 +1,17 @@
+// This tests that cpSync allows copying symlinks in src to locations in dest with
+// existing symlinks not pointing to a directory.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import { cpSync, mkdirSync, writeFileSync, symlinkSync } from 'node:fs';
+import { resolve, join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+const dest = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(`${src}/test.txt`, 'test');
+symlinkSync(resolve(`${src}/test.txt`), join(src, 'link.txt'));
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs
new file mode 100644
index 000000000000..034c54efec25
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs
@@ -0,0 +1,14 @@
+// This tests that cpSync throws error if attempt is made to copy to subdirectory of self.
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = fixtures.path('copy/kitchen-sink/a');
+assert.throws(
+  () => cpSync(src, dest),
+  { code: 'ERR_FS_CP_EINVAL' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dereference-directory.mjs b/test/js/node/test/parallel/test-fs-cp-sync-dereference-directory.mjs
new file mode 100644
index 000000000000..0fdb827301d1
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dereference-directory.mjs
@@ -0,0 +1,23 @@
+// This tests that cpSync overrides target directory with what symlink points to, when dereference is true.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, symlinkSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+const symlink = nextdir();
+const dest = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync(src, symlink);
+
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+
+cpSync(symlink, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+const destStat = lstatSync(dest);
+assert(!destStat.isSymbolicLink());
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dereference-file.mjs b/test/js/node/test/parallel/test-fs-cp-sync-dereference-file.mjs
new file mode 100644
index 000000000000..3615dde9aaad
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dereference-file.mjs
@@ -0,0 +1,23 @@
+// This tests that cpSync copies file itself, rather than symlink, when dereference is true.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'assert';
+
+import { cpSync, mkdirSync, writeFileSync, symlinkSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync(join(src, 'foo.js'), join(src, 'bar.js'));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+const destFile = join(dest, 'foo.js');
+
+cpSync(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+const stat = lstatSync(destFile);
+assert(stat.isFile());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs b/test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs
new file mode 100644
index 000000000000..921b65902f1c
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dereference-twice.mjs
@@ -0,0 +1,20 @@
+// This tests that cpSync does not fail if the same directory is copied to dest twice,
+// when dereference is true, and force is false (fails silently).
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'assert';
+
+import { cpSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+const destFile = join(dest, 'a/b/README2.md');
+cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true }));
+const stat = lstatSync(destFile);
+assert(stat.isFile());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dereference.js b/test/js/node/test/parallel/test-fs-cp-sync-dereference.js
new file mode 100644
index 000000000000..dffb5b171c4d
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dereference.js
@@ -0,0 +1,50 @@
+'use strict';
+
+// Refs: https://github.com/nodejs/node/issues/58939
+//
+// In this test, both the cp and cpSync functions are attempting to copy
+// a file over a symlinked directory.
+
+const common = require('../common');
+
+const {
+  cp,
+  cpSync,
+  mkdirSync,
+  symlinkSync,
+  writeFileSync,
+  readFileSync,
+  statSync
+} = require('fs');
+
+const {
+  join,
+} = require('path');
+
+const assert = require('assert');
+
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh();
+
+const pathA = join(tmpdir.path, 'a'); // file
+const pathB = join(tmpdir.path, 'b'); // directory
+const pathC = join(tmpdir.path, 'c'); // c -> b
+const pathD = join(tmpdir.path, 'd'); // d -> b
+
+writeFileSync(pathA, 'file a');
+mkdirSync(pathB);
+symlinkSync(pathB, pathC, 'dir');
+symlinkSync(pathB, pathD, 'dir');
+
+cp(pathA, pathD, { dereference: false }, common.mustSucceed(() => {
+  // The path d is now a file, not a symlink
+  assert.strictEqual(readFileSync(pathA, 'utf-8'), readFileSync(pathD, 'utf-8'));
+  assert.ok(statSync(pathA).isFile());
+  assert.ok(statSync(pathD).isFile());
+}));
+
+cpSync(pathA, pathC, { dereference: false });
+
+assert.strictEqual(readFileSync(pathA, 'utf-8'), readFileSync(pathC, 'utf-8'));
+assert.ok(statSync(pathA).isFile());
+assert.ok(statSync(pathC).isFile());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs b/test/js/node/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs
new file mode 100644
index 000000000000..5322188f34db
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs
@@ -0,0 +1,14 @@
+// This tests that cpSync must not throw error if attempt is made to copy to dest
+// directory with same prefix as src directory.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import { cpSync, mkdirSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir('prefix', tmpdir);
+const dest = nextdir('prefix-a', tmpdir);
+mkdirSync(src);
+mkdirSync(dest);
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs b/test/js/node/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs
new file mode 100644
index 000000000000..4b136398e654
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs
@@ -0,0 +1,16 @@
+// This tests that cpSync must not throw error if attempt is made to copy to dest
+// directory if the parent of dest has same prefix as src directory.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import { cpSync, mkdirSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir('aa', tmpdir);
+const destParent = nextdir('aaa', tmpdir);
+const dest = nextdir('aaa/aabb', tmpdir);
+mkdirSync(src);
+mkdirSync(destParent);
+mkdirSync(dest);
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs
new file mode 100644
index 000000000000..2ea2b0aaf63a
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs
@@ -0,0 +1,15 @@
+// This tests that cpSync throws an error when attempting to copy a dir that does not exist.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+const dest = nextdir();
+assert.throws(
+  () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })),
+  { code: 'ENOENT' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-error-on-exist.mjs b/test/js/node/test/parallel/test-fs-cp-sync-error-on-exist.mjs
new file mode 100644
index 000000000000..700e52e3b3c8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-error-on-exist.mjs
@@ -0,0 +1,22 @@
+// This tests that cpSync throws error if errorOnExist is true, force is false, and file or folder copied over.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assert.throws(
+  () => cpSync(src, dest, {
+    dereference: true,
+    errorOnExist: true,
+    force: false,
+    recursive: true,
+  }),
+  { code: 'ERR_FS_CP_EEXIST' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-file-url.mjs b/test/js/node/test/parallel/test-fs-cp-sync-file-url.mjs
new file mode 100644
index 000000000000..8da791b5c376
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-file-url.mjs
@@ -0,0 +1,14 @@
+// This tests that cpSync accepts file URL as src and dest.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import { cpSync } from 'node:fs';
+import { pathToFileURL } from 'node:url';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true }));
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs
new file mode 100644
index 000000000000..363a84b366f7
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs
@@ -0,0 +1,17 @@
+// This tests that cpSync throws an error when attempting to copy a file with a name that is too long.
+import '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+
+const isWindows = process.platform === 'win32';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = 'a'.repeat(5000);
+const dest = nextdir();
+assert.throws(
+  () => cpSync(src, dest),
+  { code: isWindows ? 'ENOENT' : 'ENAMETOOLONG' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs
new file mode 100644
index 000000000000..629da5d9dfd6
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs
@@ -0,0 +1,14 @@
+// This tests that cpSync throws an error when both dereference and verbatimSymlinks are enabled.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+assert.throws(
+  () => cpSync(src, src, mustNotMutateObjectDeep({ dereference: true, verbatimSymlinks: true })),
+  { code: 'ERR_INCOMPATIBLE_OPTION_PAIR' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-mode-flags.mjs b/test/js/node/test/parallel/test-fs-cp-sync-mode-flags.mjs
new file mode 100644
index 000000000000..5471a6008520
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-mode-flags.mjs
@@ -0,0 +1,30 @@
+// This tests that cpSync copies a nested folder structure with mode flags.
+// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, constants } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+try {
+  cpSync(src, dest, mustNotMutateObjectDeep({
+    recursive: true,
+    mode: constants.COPYFILE_FICLONE_FORCE,
+  }));
+} catch (err) {
+  // If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
+  // it should enter this path.
+  assert.strictEqual(err.syscall, 'copyfile');
+  assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
+    err.code === 'ENOSYS' || err.code === 'EXDEV');
+  process.exit(0);
+}
+
+// If the platform support `COPYFILE_FICLONE_FORCE` operation,
+// it should reach to here.
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-mode-invalid.mjs b/test/js/node/test/parallel/test-fs-cp-sync-mode-invalid.mjs
new file mode 100644
index 000000000000..2ebbd2431238
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-mode-invalid.mjs
@@ -0,0 +1,12 @@
+// This tests that cpSync rejects if options.mode is invalid.
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+assert.throws(
+  () => cpSync('a', 'b', { mode: -1 }),
+  { code: 'ERR_OUT_OF_RANGE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-nested-files-folders.mjs b/test/js/node/test/parallel/test-fs-cp-sync-nested-files-folders.mjs
new file mode 100644
index 000000000000..378bb5386455
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-nested-files-folders.mjs
@@ -0,0 +1,13 @@
+// This tests that cpSync copies a nested folder structure with files and folders.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs b/test/js/node/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs
new file mode 100644
index 000000000000..696d48d3edc5
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs
@@ -0,0 +1,21 @@
+// This tests that cpSync does not throw errors when directory is copied over and force is false.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'README.md'), 'hello world', 'utf8');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+const initialStat = lstatSync(join(dest, 'README.md'));
+cpSync(src, dest, mustNotMutateObjectDeep({ force: false, recursive: true }));
+// File should not have been copied over, so access times will be identical:
+assertDirEquivalent(src, dest);
+const finalStat = lstatSync(join(dest, 'README.md'));
+assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs
new file mode 100644
index 000000000000..d7d8bea38fff
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs
@@ -0,0 +1,12 @@
+// This tests that cpSync throws if options is not object.
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+assert.throws(
+  () => cpSync('a', 'b', () => {}),
+  { code: 'ERR_INVALID_ARG_TYPE' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs b/test/js/node/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs
new file mode 100644
index 000000000000..cd9f8ab3e3ee
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs
@@ -0,0 +1,19 @@
+// This tests that cpSync overwrites existing files if force is true.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8');
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assertDirEquivalent(src, dest);
+const content = readFileSync(join(dest, 'README.md'), 'utf8');
+assert.strictEqual(content.trim(), '# Hello');
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs
new file mode 100644
index 000000000000..52feaf812016
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs
@@ -0,0 +1,26 @@
+// This tests that cpSync throws error if parent directory of symlink in dest points to src.
+import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+// See https://github.com/nodejs/node/pull/48409
+if (isInsideDirWithUnusualChars) {
+  skip('Test is borken in directories with unusual characters');
+}
+
+const src = nextdir();
+mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+// Create symlink in dest pointing to src.
+const destLink = join(dest, 'b');
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, destLink);
+assert.throws(
+  () => cpSync(src, join(dest, 'b', 'c')),
+  { code: 'ERR_FS_CP_EINVAL' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs b/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs
new file mode 100644
index 000000000000..69a4de434f74
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs
@@ -0,0 +1,24 @@
+// This tests that cpSync makes file writeable when updating timestamp, if not writeable.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import { setTimeout } from 'node:timers/promises';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 }));
+// Small wait to make sure that destStat.mtime.getTime() would produce a time
+// different from srcStat.mtime.getTime() if preserveTimestamps wasn't set to true
+await setTimeout(5);
+cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true }));
+assertDirEquivalent(src, dest);
+const srcStat = lstatSync(join(src, 'foo.txt'));
+const destStat = lstatSync(join(dest, 'foo.txt'));
+assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs b/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs
new file mode 100644
index 000000000000..3fb35c2e6359
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs
@@ -0,0 +1,18 @@
+// This tests that cpSync copies timestamps from src to dest if preserveTimestamps is true.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, lstatSync } from 'node:fs';
+import { join } from 'node:path';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true }));
+assertDirEquivalent(src, dest);
+const srcStat = lstatSync(join(src, 'index.js'));
+const destStat = lstatSync(join(dest, 'index.js'));
+assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs b/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs
new file mode 100644
index 000000000000..7c7622ed4050
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs
@@ -0,0 +1,28 @@
+// This tests that cpSync resolves relative symlinks to their absolute path by default.
+import { mustNotMutateObjectDeep, isWindows } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync('foo.js', join(src, 'bar.js'));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+const link = readlinkSync(join(dest, 'bar.js'));
+
+if (isWindows) {
+  // On Windows, readlinkSync() may return a path with uppercase drive letter,
+  // but paths are case-insensitive.
+  assert.strictEqual(link.toLowerCase(), join(src, 'foo.js').toLowerCase());
+} else {
+  assert.strictEqual(link, join(src, 'foo.js'));
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs b/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs
new file mode 100644
index 000000000000..ac1ec01fd0b6
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs
@@ -0,0 +1,28 @@
+// This tests that cpSync resolves relative symlinks when verbatimSymlinks is false.
+import { mustNotMutateObjectDeep, isWindows } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync('foo.js', join(src, 'bar.js'));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: false }));
+const link = readlinkSync(join(dest, 'bar.js'));
+
+if (isWindows) {
+  // On Windows, readlinkSync() may return a path with uppercase drive letter,
+  // but paths are case-insensitive.
+  assert.strictEqual(link.toLowerCase(), join(src, 'foo.js').toLowerCase());
+} else {
+  assert.strictEqual(link, join(src, 'foo.js'));
+}
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs
new file mode 100644
index 000000000000..81d7e1fcb63e
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs
@@ -0,0 +1,14 @@
+// This tests that cpSync throws error when src and dest are identical.
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+assert.throws(
+  () => cpSync(src, src),
+  { code: 'ERR_FS_CP_EINVAL' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs
new file mode 100644
index 000000000000..55e4c7014b74
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs
@@ -0,0 +1,25 @@
+// This tests that cpSync throws error if attempt is made to copy src to dest when
+// src is parent directory of the parent of dest.
+import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync } from 'node:fs';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+// See https://github.com/nodejs/node/pull/48409
+if (isInsideDirWithUnusualChars) {
+  skip('Test is borken in directories with unusual characters');
+}
+
+const src = nextdir('a', tmpdir);
+const destParent = nextdir('a/b', tmpdir);
+const dest = nextdir('a/b/c', tmpdir);
+mkdirSync(src);
+mkdirSync(destParent);
+mkdirSync(dest);
+assert.throws(
+  () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })),
+  { code: 'ERR_FS_CP_EINVAL' },
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs
new file mode 100644
index 000000000000..580e2ada0e27
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs
@@ -0,0 +1,21 @@
+// This tests that cpSync throws error if symlink in dest points to location in src.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+const dest = nextdir();
+mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true }));
+symlinkSync(src, join(dest, 'a', 'c'));
+assert.throws(
+  () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })),
+  { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs b/test/js/node/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs
new file mode 100644
index 000000000000..141798f1d27d
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs
@@ -0,0 +1,22 @@
+// This tests that cpSync throws error if symlink in src points to location in dest.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, symlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+const dest = nextdir();
+mkdirSync(dest);
+symlinkSync(dest, join(src, 'link'));
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assert.throws(
+  () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })),
+  {
+    code: 'ERR_FS_CP_EINVAL'
+  }
+);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-unicode-dest.mjs b/test/js/node/test/parallel/test-fs-cp-sync-unicode-dest.mjs
new file mode 100644
index 000000000000..0638b98180c5
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-unicode-dest.mjs
@@ -0,0 +1,23 @@
+// Regression test for https://github.com/nodejs/node/issues/61878
+// fs.cpSync should copy files when destination path has UTF characters.
+import '../common/index.mjs';
+import { cpSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import assert from 'node:assert';
+import tmpdir from '../common/tmpdir.js';
+
+tmpdir.refresh();
+
+const src = join(tmpdir.path, 'src');
+mkdirSync(join(src, 'subdir'), { recursive: true });
+writeFileSync(join(src, 'file1.txt'), 'Hello World');
+writeFileSync(join(src, 'subdir', 'nested.txt'), 'Nested File');
+
+const dest = join(tmpdir.path, 'Eyjafjallajökull-Pranckevičius');
+cpSync(src, dest, { recursive: true, force: true });
+
+const destFiles = readdirSync(dest);
+assert.ok(destFiles.includes('file1.txt'));
+assert.strictEqual(readFileSync(join(dest, 'file1.txt'), 'utf8'), 'Hello World');
+assert.ok(destFiles.includes('subdir'));
+assert.strictEqual(readFileSync(join(dest, 'subdir', 'nested.txt'), 'utf8'), 'Nested File');
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs b/test/js/node/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs
new file mode 100644
index 000000000000..6393aeb2c158
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs
@@ -0,0 +1,13 @@
+// This tests that cpSync copies a nested folder containing UTF characters.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir, assertDirEquivalent } from '../common/fs.js';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/utf/新建文件夹');
+const dest = nextdir();
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
+assertDirEquivalent(src, dest);
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs b/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs
new file mode 100644
index 000000000000..3db176487f71
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs
@@ -0,0 +1,17 @@
+// This tests that cpSync throws error when verbatimSymlinks is not a boolean.
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { cpSync } from 'node:fs';
+import tmpdir from '../common/tmpdir.js';
+import fixtures from '../common/fixtures.js';
+
+tmpdir.refresh();
+
+const src = fixtures.path('copy/kitchen-sink');
+[1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}]
+  .forEach((verbatimSymlinks) => {
+    assert.throws(
+      () => cpSync(src, src, { verbatimSymlinks }),
+      { code: 'ERR_INVALID_ARG_TYPE' }
+    );
+  });
diff --git a/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs b/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs
new file mode 100644
index 000000000000..e8d0010119fa
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs
@@ -0,0 +1,21 @@
+// This tests that cpSync does not resolve relative symlinks when verbatimSymlinks is true.
+import { mustNotMutateObjectDeep } from '../common/index.mjs';
+import { nextdir } from '../common/fs.js';
+import assert from 'node:assert';
+import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs';
+import { join } from 'node:path';
+
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+const src = nextdir();
+mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
+writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+symlinkSync('foo.js', join(src, 'bar.js'));
+
+const dest = nextdir();
+mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true }));
+
+cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: true }));
+const link = readlinkSync(join(dest, 'bar.js'));
+assert.strictEqual(link, 'foo.js');
diff --git a/test/js/node/test/parallel/test-fs-fchown-negative-one.js b/test/js/node/test/parallel/test-fs-fchown-negative-one.js
new file mode 100644
index 000000000000..e88b3a2428fd
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-fchown-negative-one.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFilePath = path.join(tmpdir.path, 'fchown-test-file.txt');
+
+fs.writeFileSync(testFilePath, 'test content for fchown');
+
+{
+  const fd = fs.openSync(testFilePath, 'r+');
+  const stats = fs.fstatSync(fd);
+  const uid = stats.uid;
+  const gid = stats.gid;
+
+  fs.fchown(fd, -1, -1, common.mustSucceed(() => {
+    const stats = fs.fstatSync(fd);
+    assert.strictEqual(stats.uid, uid);
+    assert.strictEqual(stats.gid, gid);
+    fs.closeSync(fd);
+  }));
+}
+
+// Test sync fchown with -1 values
+{
+  const fd = fs.openSync(testFilePath, 'r+');
+  const stats = fs.fstatSync(fd);
+  const uid = stats.uid;
+  const gid = stats.gid;
+
+  fs.fchownSync(fd, -1, -1);
+  const statsAfter = fs.fstatSync(fd);
+  assert.strictEqual(statsAfter.uid, uid);
+  assert.strictEqual(statsAfter.gid, gid);
+
+  fs.closeSync(fd);
+}
diff --git a/test/js/node/test/parallel/test-fs-fmap.js b/test/js/node/test/parallel/test-fs-fmap.js
new file mode 100644
index 000000000000..c4298f0d0e28
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-fmap.js
@@ -0,0 +1,28 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const fs = require('fs');
+
+const {
+  O_CREAT = 0,
+  O_RDONLY = 0,
+  O_TRUNC = 0,
+  O_WRONLY = 0,
+  UV_FS_O_FILEMAP = 0
+} = fs.constants;
+
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh();
+
+// Run this test on all platforms. While UV_FS_O_FILEMAP is only available on
+// Windows, it should be silently ignored on other platforms.
+
+const filename = tmpdir.resolve('fmap.txt');
+const text = 'Memory File Mapping Test';
+
+const mw = UV_FS_O_FILEMAP | O_TRUNC | O_CREAT | O_WRONLY;
+const mr = UV_FS_O_FILEMAP | O_RDONLY;
+
+fs.writeFileSync(filename, text, { flag: mw });
+const r1 = fs.readFileSync(filename, { encoding: 'utf8', flag: mr });
+assert.strictEqual(r1, text);
diff --git a/test/js/node/test/parallel/test-fs-glob-throw.mjs b/test/js/node/test/parallel/test-fs-glob-throw.mjs
new file mode 100644
index 000000000000..f7fa64d62dab
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-glob-throw.mjs
@@ -0,0 +1,16 @@
+import { mustCall } from '../common/index.mjs';
+import { glob } from 'node:fs';
+import process from 'node:process';
+import assert from 'node:assert';
+
+// One uncaught error is expected
+process.on('uncaughtException', mustCall((err) => {
+  assert.strictEqual(err.message, 'blep');
+}));
+
+{
+  // Test that if callback throws, it's not getting called again
+  glob('a/b/c', mustCall(() => {
+    throw new Error('blep');
+  }));
+}
diff --git a/test/js/node/test/parallel/test-fs-glob.mjs b/test/js/node/test/parallel/test-fs-glob.mjs
new file mode 100644
index 000000000000..560b4e72e4ad
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-glob.mjs
@@ -0,0 +1,671 @@
+import * as common from '../common/index.mjs';
+import tmpdir from '../common/tmpdir.js';
+import { resolve, dirname, sep, relative, join, isAbsolute } from 'node:path';
+import { mkdir, writeFile, symlink, glob as asyncGlob } from 'node:fs/promises';
+import { glob, globSync, Dirent, chmodSync, writeFileSync, rmSync } from 'node:fs';
+import { test, describe } from 'node:test';
+import { pathToFileURL } from 'node:url';
+import { promisify } from 'node:util';
+import assert from 'node:assert';
+
+function assertDirents(dirents) {
+  assert.ok(dirents.every((dirent) => dirent instanceof Dirent));
+}
+
+tmpdir.refresh();
+
+const fixtureDir = tmpdir.resolve('fixtures');
+const absDir = tmpdir.resolve('abs');
+
+async function setup() {
+  await mkdir(fixtureDir, { recursive: true });
+  await mkdir(absDir, { recursive: true });
+  const files = [
+    'a/.abcdef/x/y/z/a',
+    'a/abcdef/g/h',
+    'a/abcfed/g/h',
+    'a/b/c/d',
+    'a/bc/e/f',
+    'a/c/d/c/b',
+    'a/cb/e/f',
+    'a/x/.y/b',
+    'a/z/.y/b',
+    'a/.b',
+    'a/b/.b',
+  ].map((f) => resolve(fixtureDir, f));
+
+  const symlinkTo = resolve(fixtureDir, 'a/symlink/a/b/c');
+  const symlinkFrom = '../..';
+  const followTarget = resolve(fixtureDir, 'follow/target');
+  const followLink = resolve(fixtureDir, 'follow/link');
+  const followCycle = resolve(fixtureDir, 'follow/cycle');
+
+  for (const file of files) {
+    const f = resolve(fixtureDir, file);
+    const d = dirname(f);
+    await mkdir(d, { recursive: true });
+    await writeFile(f, 'i like tests');
+  }
+
+  await mkdir(followTarget, { recursive: true });
+  await writeFile(resolve(followTarget, 'file.txt'), 'follow symlinks');
+
+  if (!common.isWindows) {
+    const d = dirname(symlinkTo);
+    await mkdir(d, { recursive: true });
+    await symlink(symlinkFrom, symlinkTo, 'dir');
+  }
+
+  const linkType = common.isWindows ? 'junction' : 'dir';
+  await symlink(followTarget, followLink, linkType);
+  await symlink(resolve(fixtureDir, 'follow'), followCycle, linkType);
+
+  await Promise.all(['foo', 'bar', 'baz', 'asdf', 'quux', 'qwer', 'rewq'].map(async function(w) {
+    await mkdir(resolve(absDir, w), { recursive: true });
+  }));
+}
+
+await setup();
+
+const patterns = {
+  'a/c/d/*/b': ['a/c/d/c/b'],
+  'a//c//d//*//b': ['a/c/d/c/b'],
+  'a/*/d/*/b': ['a/c/d/c/b'],
+  'a/*/+(c|g)/./d': ['a/b/c/d'],
+  'a/**/[cg]/../[cg]': [
+    'a/abcdef/g',
+    'a/abcfed/g',
+    'a/b/c',
+    'a/c',
+    'a/c/d/c',
+    common.isWindows ? null : 'a/symlink/a/b/c',
+  ],
+  'a/{b,c,d,e,f}/**/g': [],
+  'a/b/**': ['a/b', 'a/b/c', 'a/b/c/d'],
+  'a/{b/**,b/c}': ['a/b', 'a/b/c', 'a/b/c/d'],
+  './**/g': ['a/abcdef/g', 'a/abcfed/g'],
+  'a/abc{fed,def}/g/h': ['a/abcdef/g/h', 'a/abcfed/g/h'],
+  'a/abc{fed/g,def}/**/': ['a/abcdef', 'a/abcdef/g', 'a/abcfed/g'],
+  'a/abc{fed/g,def}/**///**/': ['a/abcdef', 'a/abcdef/g', 'a/abcfed/g'],
+  '**/a': common.isWindows ? ['a'] : ['a', 'a/symlink/a'],
+  '**/a/**': [
+    'a',
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcdef/g/h',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/abcfed/g/h',
+    'a/b',
+    'a/b/c',
+    'a/b/c/d',
+    'a/bc',
+    'a/bc/e',
+    'a/bc/e/f',
+    'a/c',
+    'a/c/d',
+    'a/c/d/c',
+    'a/c/d/c/b',
+    'a/cb',
+    'a/cb/e',
+    'a/cb/e/f',
+    ...(common.isWindows ? [] : [
+      'a/symlink',
+      'a/symlink/a',
+      'a/symlink/a/b',
+      'a/symlink/a/b/c',
+    ]),
+    'a/x',
+    'a/z',
+  ],
+  './**/a': common.isWindows ? ['a'] : ['a', 'a/symlink/a', 'a/symlink/a/b/c/a'],
+  './**/a/**/': [
+    'a',
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/b',
+    'a/b/c',
+    'a/bc',
+    'a/bc/e',
+    'a/c',
+    'a/c/d',
+    'a/c/d/c',
+    'a/cb',
+    'a/cb/e',
+    ...(common.isWindows ? [] : [
+      'a/symlink',
+      'a/symlink/a',
+      'a/symlink/a/b',
+      'a/symlink/a/b/c',
+      'a/symlink/a/b/c/a',
+      'a/symlink/a/b/c/a/b',
+      'a/symlink/a/b/c/a/b/c',
+    ]),
+    'a/x',
+    'a/z',
+  ],
+  './**/a/**': [
+    'a',
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcdef/g/h',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/abcfed/g/h',
+    'a/b',
+    'a/b/c',
+    'a/b/c/d',
+    'a/bc',
+    'a/bc/e',
+    'a/bc/e/f',
+    'a/c',
+    'a/c/d',
+    'a/c/d/c',
+    'a/c/d/c/b',
+    'a/cb',
+    'a/cb/e',
+    'a/cb/e/f',
+    ...(common.isWindows ? [] : [
+      'a/symlink',
+      'a/symlink/a',
+      'a/symlink/a/b',
+      'a/symlink/a/b/c',
+      'a/symlink/a/b/c/a',
+      'a/symlink/a/b/c/a/b',
+      'a/symlink/a/b/c/a/b/c',
+    ]),
+    'a/x',
+    'a/z',
+  ],
+  './**/a/**/a/**/': common.isWindows ? [] : [
+    'a/symlink/a',
+    'a/symlink/a/b',
+    'a/symlink/a/b/c',
+    'a/symlink/a/b/c/a',
+    'a/symlink/a/b/c/a/b',
+    'a/symlink/a/b/c/a/b/c',
+    'a/symlink/a/b/c/a/b/c/a',
+    'a/symlink/a/b/c/a/b/c/a/b',
+    'a/symlink/a/b/c/a/b/c/a/b/c',
+  ],
+  '+(a|b|c)/a{/,bc*}/**': [
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcdef/g/h',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/abcfed/g/h',
+  ],
+  '*/*/*/f': ['a/bc/e/f', 'a/cb/e/f'],
+  './**/f': ['a/bc/e/f', 'a/cb/e/f'],
+  '**/.b': ['a/.b', 'a/b/.b'],
+  './**/.b': ['a/.b', 'a/b/.b'],
+  'a/**/.b': ['a/.b', 'a/b/.b'],
+  'a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**': common.isWindows ? [] : [
+    'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c',
+    'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a',
+    'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b',
+    'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c',
+  ],
+  [`{./*/*,${absDir}/*}`]: [
+    `${absDir}/asdf`,
+    `${absDir}/bar`,
+    `${absDir}/baz`,
+    `${absDir}/foo`,
+    `${absDir}/quux`,
+    `${absDir}/qwer`,
+    `${absDir}/rewq`,
+    'a/abcdef',
+    'a/abcfed',
+    'a/b',
+    'a/bc',
+    'a/c',
+    'a/cb',
+    common.isWindows ? null : 'a/symlink',
+    'a/x',
+    'a/z',
+    'follow/cycle',
+    'follow/link',
+    'follow/target',
+  ],
+  [`{${absDir}/*,*}`]: [
+    `${absDir}/asdf`,
+    `${absDir}/bar`,
+    `${absDir}/baz`,
+    `${absDir}/foo`,
+    `${absDir}/quux`,
+    `${absDir}/qwer`,
+    `${absDir}/rewq`,
+    'a',
+    'follow',
+  ],
+  'a/!(symlink)/**': [
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcdef/g/h',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/abcfed/g/h',
+    'a/b',
+    'a/b/c',
+    'a/b/c/d',
+    'a/bc',
+    'a/bc/e',
+    'a/bc/e/f',
+    'a/c',
+    'a/c/d',
+    'a/c/d/c',
+    'a/c/d/c/b',
+    'a/cb',
+    'a/cb/e',
+    'a/cb/e/f',
+    'a/x',
+    'a/z',
+  ],
+  'a/symlink/a/**/*': common.isWindows ? [] : [
+    'a/symlink/a/b',
+    'a/symlink/a/b/c',
+    'a/symlink/a/b/c/a',
+  ],
+  'a/!(symlink)/**/..': [
+    'a',
+    'a/abcdef',
+    'a/abcfed',
+    'a/b',
+    'a/bc',
+    'a/c',
+    'a/c/d',
+    'a/cb',
+  ],
+  'a/!(symlink)/**/../': [
+    'a',
+    'a/abcdef',
+    'a/abcfed',
+    'a/b',
+    'a/bc',
+    'a/c',
+    'a/c/d',
+    'a/cb',
+  ],
+  'a/!(symlink)/**/../*': [
+    'a/abcdef',
+    'a/abcdef/g',
+    'a/abcfed',
+    'a/abcfed/g',
+    'a/b',
+    'a/b/c',
+    'a/bc',
+    'a/bc/e',
+    'a/c',
+    'a/c/d',
+    'a/c/d/c',
+    'a/cb',
+    'a/cb/e',
+    common.isWindows ? null : 'a/symlink',
+    'a/x',
+    'a/z',
+  ],
+  'a/!(symlink)/**/../*/*': [
+    'a/abcdef/g',
+    'a/abcdef/g/h',
+    'a/abcfed/g',
+    'a/abcfed/g/h',
+    'a/b/c',
+    'a/b/c/d',
+    'a/bc/e',
+    'a/bc/e/f',
+    'a/c/d',
+    'a/c/d/c',
+    'a/c/d/c/b',
+    'a/cb/e',
+    'a/cb/e/f',
+    common.isWindows ? null : 'a/symlink/a',
+  ],
+};
+
+describe('glob', function() {
+  const promisified = promisify(glob);
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = (await promisified(pattern, { cwd: fixtureDir })).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('globSync', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, () => {
+      const actual = globSync(pattern, { cwd: fixtureDir }).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('fsPromises glob', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = [];
+      for await (const item of asyncGlob(pattern, { cwd: fixtureDir })) actual.push(item);
+      actual.sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('glob - with file: URL as cwd', function() {
+  const promisified = promisify(glob);
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = (await promisified(pattern, { cwd: pathToFileURL(fixtureDir) })).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('globSync - with file: URL as cwd', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, () => {
+      const actual = globSync(pattern, { cwd: pathToFileURL(fixtureDir) }).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('fsPromises.glob - with file: URL as cwd', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = [];
+      for await (const item of asyncGlob(pattern, { cwd: pathToFileURL(fixtureDir) })) actual.push(item);
+      actual.sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+const normalizeDirent = (dirent) => relative(fixtureDir, join(dirent.parentPath, dirent.name));
+// The call to `join()` with only one argument is important, as
+// it ensures that the proper path seperators are applied.
+const normalizePath = (path) => (isAbsolute(path) ? relative(fixtureDir, path) : join(path));
+
+describe('glob - withFileTypes', function() {
+  const promisified = promisify(glob);
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = await promisified(pattern, {
+        cwd: fixtureDir,
+        withFileTypes: true,
+        exclude: common.mustCallAtLeast((dirent) => assert.ok(dirent instanceof Dirent), 0),
+      });
+      assertDirents(actual);
+      assert.deepStrictEqual(actual.map(normalizeDirent).sort(), expected.filter(Boolean).map(normalizePath).sort());
+    });
+  }
+});
+
+describe('globSync - withFileTypes', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, () => {
+      const actual = globSync(pattern, {
+        cwd: fixtureDir,
+        withFileTypes: true,
+        exclude: common.mustCallAtLeast((dirent) => assert.ok(dirent instanceof Dirent), 0),
+      });
+      assertDirents(actual);
+      assert.deepStrictEqual(actual.map(normalizeDirent).sort(), expected.filter(Boolean).map(normalizePath).sort());
+    });
+  }
+});
+
+describe('fsPromises glob - withFileTypes', function() {
+  for (const [pattern, expected] of Object.entries(patterns)) {
+    test(pattern, async () => {
+      const actual = [];
+      for await (const item of asyncGlob(pattern, {
+        cwd: fixtureDir,
+        withFileTypes: true,
+        exclude: common.mustCallAtLeast((dirent) => assert.ok(dirent instanceof Dirent), 0),
+      })) actual.push(item);
+      assertDirents(actual);
+      assert.deepStrictEqual(actual.map(normalizeDirent).sort(), expected.filter(Boolean).map(normalizePath).sort());
+    });
+  }
+});
+
+// [pattern, exclude option, expected result]
+const patterns2 = [
+  ['a/{b,c}*', ['a/*c'], ['a/b', 'a/cb']],
+  ['a/{a,b,c}*', ['a/*bc*', 'a/cb'], ['a/b', 'a/c']],
+  ['a/**/[cg]', ['**/c'], ['a/abcdef/g', 'a/abcfed/g']],
+  ['a/**/[cg]', ['./**/c'], ['a/abcdef/g', 'a/abcfed/g']],
+  ['a/**/[cg]', ['a/**/[cg]/../c'], ['a/abcdef/g', 'a/abcfed/g']],
+  ['a/*/+(c|g)/*', ['**/./h'], ['a/b/c/d']],
+  [
+    'a/**/[cg]/../[cg]',
+    ['a/ab{cde,cfe}*'],
+    [
+      'a/b/c',
+      'a/c',
+      'a/c/d/c',
+      ...(common.isWindows ? [] : ['a/symlink/a/b/c']),
+    ],
+  ],
+  [
+    `${absDir}/*`,
+    [`${absDir}/asdf`, `${absDir}/ba*`],
+    [`${absDir}/foo`, `${absDir}/quux`, `${absDir}/qwer`, `${absDir}/rewq`],
+  ],
+  [
+    `${absDir}/*`,
+    [`${absDir}/asdf`, `**/ba*`],
+    [
+      `${absDir}/bar`,
+      `${absDir}/baz`,
+      `${absDir}/foo`,
+      `${absDir}/quux`,
+      `${absDir}/qwer`,
+      `${absDir}/rewq`,
+    ],
+  ],
+  [
+    [`${absDir}/*`, 'a/**/[cg]'],
+    [`${absDir}/*{a,q}*`, './a/*{c,b}*/*'],
+    [`${absDir}/foo`, 'a/c', ...(common.isWindows ? [] : ['a/symlink/a/b/c'])],
+  ],
+  [ 'a/**', () => true, [] ],
+  [ 'a/**', [ '*' ], [] ],
+  [ 'a/**', [ '**' ], [] ],
+  [ 'a/**', [ 'a/**' ], [] ],
+];
+
+describe('globSync - exclude', function() {
+  for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
+    test(`${pattern} - exclude: ${exclude}`, () => {
+      const actual = globSync(pattern, { cwd: fixtureDir, exclude }).sort();
+      assert.strictEqual(actual.length, 0);
+    });
+  }
+  for (const [pattern, exclude, expected] of patterns2) {
+    test(`${pattern} - exclude: ${exclude}`, () => {
+      const actual = globSync(pattern, { cwd: fixtureDir, exclude }).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('glob - exclude', function() {
+  const promisified = promisify(glob);
+  for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
+    test(`${pattern} - exclude: ${exclude}`, async () => {
+      const actual = (await promisified(pattern, { cwd: fixtureDir, exclude })).sort();
+      assert.strictEqual(actual.length, 0);
+    });
+  }
+  for (const [pattern, exclude, expected] of patterns2) {
+    test(`${pattern} - exclude: ${exclude}`, async () => {
+      const actual = (await promisified(pattern, { cwd: fixtureDir, exclude })).sort();
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual, normalized);
+    });
+  }
+});
+
+describe('fsPromises glob - exclude', function() {
+  for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
+    test(`${pattern} - exclude: ${exclude}`, async () => {
+      const actual = [];
+      for await (const item of asyncGlob(pattern, { cwd: fixtureDir, exclude })) actual.push(item);
+      actual.sort();
+      assert.strictEqual(actual.length, 0);
+    });
+  }
+  for (const [pattern, exclude, expected] of patterns2) {
+    test(`${pattern} - exclude: ${exclude}`, async () => {
+      const actual = [];
+      for await (const item of asyncGlob(pattern, { cwd: fixtureDir, exclude })) actual.push(item);
+      const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
+      assert.deepStrictEqual(actual.sort(), normalized);
+    });
+  }
+});
+
+const followSymlinkPattern = 'follow/**';
+const followSymlinkExpected = [
+  'follow',
+  'follow/cycle',
+  'follow/link',
+  'follow/target',
+  'follow/target/file.txt',
+].map((item) => item.replaceAll('/', sep)).sort();
+const followSymlinkExpectedWithFollow = [
+  ...followSymlinkExpected,
+  'follow/link/file.txt'.replaceAll('/', sep),
+].sort();
+
+const getNestedCycleMatches = (matches) => {
+  return matches.filter((match) => match.startsWith(`follow${sep}cycle${sep}`));
+};
+
+describe('glob - followSymlinks', function() {
+  const promisified = promisify(glob);
+
+  test('does not follow symlinks by default', async () => {
+    const actual = (await promisified(followSymlinkPattern, { cwd: fixtureDir })).sort();
+    assert.deepStrictEqual(actual, followSymlinkExpected);
+  });
+
+  test('follows symlinked directories when enabled', async () => {
+    const actual = (await promisified(followSymlinkPattern, {
+      cwd: fixtureDir,
+      followSymlinks: true,
+    })).sort();
+    assert.deepStrictEqual(actual, followSymlinkExpectedWithFollow);
+    assert.deepStrictEqual(getNestedCycleMatches(actual), []);
+  });
+});
+
+describe('globSync - followSymlinks', function() {
+  test('does not follow symlinks by default', () => {
+    const actual = globSync(followSymlinkPattern, { cwd: fixtureDir }).sort();
+    assert.deepStrictEqual(actual, followSymlinkExpected);
+  });
+
+  test('validates followSymlinks', () => {
+    assert.throws(() => {
+      globSync(followSymlinkPattern, {
+        cwd: fixtureDir,
+        followSymlinks: 1,
+      });
+    }, {
+      code: 'ERR_INVALID_ARG_TYPE',
+    });
+  });
+
+  test('follows symlinked directories when enabled', () => {
+    const actual = globSync(followSymlinkPattern, {
+      cwd: fixtureDir,
+      followSymlinks: true,
+    }).sort();
+    assert.deepStrictEqual(actual, followSymlinkExpectedWithFollow);
+    assert.deepStrictEqual(getNestedCycleMatches(actual), []);
+  });
+
+  test('supports withFileTypes when following symlinked directories', () => {
+    const actual = globSync(followSymlinkPattern, {
+      cwd: fixtureDir,
+      followSymlinks: true,
+      withFileTypes: true,
+    });
+    assertDirents(actual);
+    const normalized = actual.map(normalizeDirent).sort();
+    assert.deepStrictEqual(normalized, followSymlinkExpectedWithFollow);
+    assert.deepStrictEqual(getNestedCycleMatches(normalized), []);
+  });
+});
+
+describe('fsPromises glob - followSymlinks', function() {
+  test('does not follow symlinks by default', async () => {
+    const actual = [];
+    for await (const item of asyncGlob(followSymlinkPattern, { cwd: fixtureDir })) actual.push(item);
+    actual.sort();
+    assert.deepStrictEqual(actual, followSymlinkExpected);
+  });
+
+  test('follows symlinked directories when enabled', async () => {
+    const actual = [];
+    for await (const item of asyncGlob(followSymlinkPattern, {
+      cwd: fixtureDir,
+      followSymlinks: true,
+    })) actual.push(item);
+    actual.sort();
+    assert.deepStrictEqual(actual, followSymlinkExpectedWithFollow);
+    assert.deepStrictEqual(getNestedCycleMatches(actual), []);
+  });
+});
+
+describe('glob - with restricted directory', function() {
+  test('*', async () => {
+    const restrictedDir = tmpdir.resolve('restricted');
+    await mkdir(restrictedDir, { recursive: true });
+    chmodSync(restrictedDir, 0o000);
+    try {
+      const results = [];
+      for await (const match of asyncGlob('*', { cwd: restrictedDir })) {
+        results.push(match);
+      }
+    } finally {
+      try {
+        chmodSync(restrictedDir, 0o755);
+      } catch {
+        // ignore
+      }
+    }
+  });
+});
+
+describe('globSync - ENOTDIR', function() {
+  test('should return empty array when a file is treated as a directory', () => {
+    const file = tmpdir.resolve('foo');
+    writeFileSync(file, '');
+    try {
+      const pattern = 'foo{,/bar}';
+      const actual = globSync(pattern, { cwd: tmpdir.path }).sort();
+      assert.deepStrictEqual(actual, ['foo']);
+    } finally {
+      try {
+        rmSync(file);
+      } catch {
+        // ignore
+      }
+    }
+  });
+});
diff --git a/test/js/node/test/parallel/test-fs-internal-assertencoding.js b/test/js/node/test/parallel/test-fs-internal-assertencoding.js
new file mode 100644
index 000000000000..8bd0a68b23a8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-internal-assertencoding.js
@@ -0,0 +1,15 @@
+'use strict';
+
+// Tests to verify that a correctly formatted invalid encoding error
+// is thrown. Originally the internal assertEncoding utility was
+// reversing the arguments when constructing the ERR_INVALID_ARG_VALUE
+// error.
+
+require('../common');
+const { opendirSync } = require('node:fs');
+const assert = require('node:assert');
+
+assert.throws(() => opendirSync('.', { encoding: 'no' }), {
+  code: 'ERR_INVALID_ARG_VALUE',
+  message: 'The argument \'encoding\' is invalid encoding. Received \'no\'',
+});
diff --git a/test/js/node/test/parallel/test-fs-lchown-negative-one.js b/test/js/node/test/parallel/test-fs-lchown-negative-one.js
new file mode 100644
index 000000000000..d681f3e429b3
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-lchown-negative-one.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFile = path.join(tmpdir.path, 'lchown-test-file.txt');
+const testLink = path.join(tmpdir.path, 'lchown-test-link');
+
+fs.writeFileSync(testFile, 'test content for lchown');
+fs.symlinkSync(testFile, testLink);
+
+const stats = fs.lstatSync(testLink);
+const uid = stats.uid;
+const gid = stats.gid;
+
+// -1 for uid and gid means "don't change the value"
+{
+  fs.lchown(testLink, -1, -1, common.mustSucceed(() => {
+    const stats = fs.lstatSync(testLink);
+    assert.strictEqual(stats.uid, uid);
+    assert.strictEqual(stats.gid, gid);
+  }));
+}
+{
+  fs.lchownSync(testLink, -1, -1);
+  const stats = fs.lstatSync(testLink);
+  assert.strictEqual(stats.uid, uid);
+  assert.strictEqual(stats.gid, gid);
+}
diff --git a/test/js/node/test/parallel/test-fs-long-path.js b/test/js/node/test/parallel/test-fs-long-path.js
index 11724a88dc4c..e903eea2e53e 100644
--- a/test/js/node/test/parallel/test-fs-long-path.js
+++ b/test/js/node/test/parallel/test-fs-long-path.js
@@ -48,5 +48,5 @@ fs.writeFile(fullPath, 'ok', common.mustSucceed(() => {
   fs.realpath.native(fullPath, common.mustSucceed());
 
   // Tests https://github.com/nodejs/node/issues/51031
-  fs.promises.realpath(fullPath).then(common.mustCall(), common.mustNotCall());
+  fs.promises.realpath(fullPath).then(common.mustCall());
 }));
diff --git a/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js b/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js
index 034a23094883..3e02401f0030 100644
--- a/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js
+++ b/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js
@@ -61,10 +61,10 @@ function makeDirectoryWritable(dir) {
   const dir = tmpdir.resolve(`mkdirp_${n++}`);
   fs.mkdirSync(dir);
   const codeExpected = makeDirectoryReadOnly(dir);
-  fs.mkdir(path.join(dir, '/bar'), { recursive: true }, (err) => {
+  fs.mkdir(path.join(dir, '/bar'), { recursive: true }, common.mustCall((err) => {
     makeDirectoryWritable(dir);
     assert(err);
     assert.strictEqual(err.code, codeExpected);
     assert(err.path);
-  });
+  }));
 }
diff --git a/test/js/node/test/parallel/test-fs-mkdtempDisposableSync.js b/test/js/node/test/parallel/test-fs-mkdtempDisposableSync.js
new file mode 100644
index 000000000000..15d4ff415da1
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-mkdtempDisposableSync.js
@@ -0,0 +1,92 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { isMainThread } = require('worker_threads');
+
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh();
+
+// Basic usage
+{
+  const result = fs.mkdtempDisposableSync(tmpdir.resolve('foo.'));
+
+  assert.strictEqual(path.basename(result.path).length, 'foo.XXXXXX'.length);
+  assert.strictEqual(path.dirname(result.path), tmpdir.path);
+  assert(fs.existsSync(result.path));
+
+  result.remove();
+
+  assert(!fs.existsSync(result.path));
+
+  // Second removal does not throw error
+  result.remove();
+}
+
+// Usage with [Symbol.dispose]()
+{
+  const result = fs.mkdtempDisposableSync(tmpdir.resolve('foo.'));
+
+  assert(fs.existsSync(result.path));
+
+  result[Symbol.dispose]();
+
+  assert(!fs.existsSync(result.path));
+
+  // Second removal does not throw error
+  result[Symbol.dispose]();
+}
+
+// `chdir`` does not affect removal
+// Can't use chdir in workers
+if (isMainThread) {
+  const originalCwd = process.cwd();
+
+  process.chdir(tmpdir.path);
+  const first = fs.mkdtempDisposableSync('first.');
+  const second = fs.mkdtempDisposableSync('second.');
+
+  const fullFirstPath = path.join(tmpdir.path, first.path);
+  const fullSecondPath = path.join(tmpdir.path, second.path);
+
+  assert(fs.existsSync(fullFirstPath));
+  assert(fs.existsSync(fullSecondPath));
+
+  process.chdir(fullFirstPath);
+  second.remove();
+
+  assert(!fs.existsSync(fullSecondPath));
+
+  process.chdir(tmpdir.path);
+  first.remove();
+  assert(!fs.existsSync(fullFirstPath));
+
+  process.chdir(originalCwd);
+}
+
+// Errors from cleanup are thrown
+// It is difficult to arrange for rmdir to fail on windows
+if (!common.isWindows && process.getuid() !== 0) {
+  const base = fs.mkdtempDisposableSync(tmpdir.resolve('foo.'));
+
+  // On Unix we can prevent removal by making the parent directory read-only
+  const child = fs.mkdtempDisposableSync(path.join(base.path, 'bar.'));
+
+  const originalMode = fs.statSync(base.path).mode;
+  fs.chmodSync(base.path, 0o444);
+
+  assert.throws(() => {
+    child.remove();
+  }, /EACCES|EPERM/);
+
+  fs.chmodSync(base.path, originalMode);
+
+  // Removal works once permissions are reset
+  child.remove();
+  assert(!fs.existsSync(child.path));
+
+  base.remove();
+  assert(!fs.existsSync(base.path));
+}
diff --git a/test/js/node/test/parallel/test-fs-open.js b/test/js/node/test/parallel/test-fs-open.js
index 56157b0183de..2d9243955e66 100644
--- a/test/js/node/test/parallel/test-fs-open.js
+++ b/test/js/node/test/parallel/test-fs-open.js
@@ -53,7 +53,7 @@ async function promise() {
   await (await fs.promises.open(__filename, 'r')).close();
 }
 
-promise().then(common.mustCall()).catch(common.mustNotCall());
+promise().then(common.mustCall());
 
 assert.throws(
   () => fs.open(__filename, 'r', 'boom', common.mustNotCall()),
diff --git a/test/js/node/test/parallel/test-fs-opendir.js b/test/js/node/test/parallel/test-fs-opendir.js
new file mode 100644
index 000000000000..5e3a92dab42f
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-opendir.js
@@ -0,0 +1,309 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const process = require('node:process');
+
+const tmpdir = require('../common/tmpdir');
+
+const testDir = tmpdir.path;
+const files = ['empty', 'files', 'for', 'just', 'testing'];
+
+process.on('warning', (cause) => {
+  // If any directory handle was left unclosed and then GC'd,
+  // it will emit `Warning: Closing directory handle on garbage collection`.
+  // Treat this warning as error.
+  throw new Error('Expected no warnings', { cause });
+});
+
+// Make sure tmp directory is clean
+tmpdir.refresh();
+
+// Create the necessary files
+files.forEach(function(filename) {
+  fs.closeSync(fs.openSync(path.join(testDir, filename), 'w'));
+});
+
+function assertDir(dir) {
+  assert(dir instanceof fs.Dir);
+  assert.throws(() => dir.constructor.prototype.path, {
+    code: 'ERR_INVALID_THIS',
+  });
+}
+
+function assertDirent(dirent) {
+  assert(dirent instanceof fs.Dirent);
+  assert.strictEqual(dirent.isFile(), true);
+  assert.strictEqual(dirent.isDirectory(), false);
+  assert.strictEqual(dirent.isSocket(), false);
+  assert.strictEqual(dirent.isBlockDevice(), false);
+  assert.strictEqual(dirent.isCharacterDevice(), false);
+  assert.strictEqual(dirent.isFIFO(), false);
+  assert.strictEqual(dirent.isSymbolicLink(), false);
+}
+
+const dirclosedError = {
+  code: 'ERR_DIR_CLOSED'
+};
+
+const dirconcurrentError = {
+  code: 'ERR_DIR_CONCURRENT_OPERATION'
+};
+
+const invalidCallbackObj = {
+  code: 'ERR_INVALID_ARG_TYPE',
+  name: 'TypeError'
+};
+
+// Check the opendir Sync version
+{
+  const dir = fs.opendirSync(testDir);
+  assertDir(dir);
+  const entries = files.map(() => {
+    const dirent = dir.readSync();
+    assertDirent(dirent);
+    return { name: dirent.name, parentPath: dirent.parentPath, toString() { return dirent.name; } };
+  }).sort();
+  assert.deepStrictEqual(entries.map((d) => d.name), files);
+  assert.deepStrictEqual(entries.map((d) => d.parentPath), Array(entries.length).fill(testDir));
+
+  // dir.read should return null when no more entries exist
+  assert.strictEqual(dir.readSync(), null);
+
+  // check .path
+  assert.strictEqual(dir.path, testDir);
+
+  dir.closeSync();
+
+  assert.throws(() => dir.readSync(), dirclosedError);
+  assert.throws(() => dir.closeSync(), dirclosedError);
+}
+
+// Check the opendir async version
+fs.opendir(testDir, common.mustSucceed((dir) => {
+  assertDir(dir);
+  let sync = true;
+  dir.read(common.mustSucceed((dirent) => {
+    assert(!sync);
+
+    // Order is operating / file system dependent
+    assert(files.includes(dirent.name), `'files' should include ${dirent}`);
+    assertDirent(dirent);
+
+    let syncInner = true;
+    dir.read(common.mustSucceed((dirent) => {
+      assert(!syncInner);
+
+      dir.close(common.mustSucceed());
+    }));
+    syncInner = false;
+  }));
+  sync = false;
+}));
+
+// opendir() on file should throw ENOTDIR
+assert.throws(function() {
+  fs.opendirSync(__filename);
+}, /Error: ENOTDIR: not a directory/);
+
+assert.throws(function() {
+  fs.opendir(__filename);
+}, /TypeError \[ERR_INVALID_ARG_TYPE\]: The "callback" argument must be of type function/);
+
+fs.opendir(__filename, common.mustCall(function(e) {
+  assert.strictEqual(e.code, 'ENOTDIR');
+}));
+
+[false, 1, [], {}, null, undefined].forEach((i) => {
+  assert.throws(
+    () => fs.opendir(i, common.mustNotCall()),
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      name: 'TypeError'
+    }
+  );
+  assert.throws(
+    () => fs.opendirSync(i),
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      name: 'TypeError'
+    }
+  );
+});
+
+// Promise-based tests
+async function doPromiseTest() {
+  // Check the opendir Promise version
+  const dir = await fs.promises.opendir(testDir);
+  assertDir(dir);
+  const entries = [];
+
+  let i = files.length;
+  while (i--) {
+    const dirent = await dir.read();
+    entries.push(dirent.name);
+    assertDirent(dirent);
+  }
+
+  assert.deepStrictEqual(files, entries.sort());
+
+  // dir.read should return null when no more entries exist
+  assert.strictEqual(await dir.read(), null);
+
+  await dir.close();
+}
+doPromiseTest().then(common.mustCall());
+
+// Async iterator
+async function doAsyncIterTest() {
+  const entries = [];
+  for await (const dirent of await fs.promises.opendir(testDir)) {
+    entries.push(dirent.name);
+    assertDirent(dirent);
+  }
+
+  assert.deepStrictEqual(files, entries.sort());
+
+  // Automatically closed during iterator
+}
+doAsyncIterTest().then(common.mustCall());
+
+// Async iterators should do automatic cleanup
+
+async function doAsyncIterBreakTest() {
+  const dir = await fs.promises.opendir(testDir);
+  for await (const dirent of dir) { // eslint-disable-line no-unused-vars
+    break;
+  }
+
+  await assert.rejects(dir.read(), dirclosedError);
+}
+doAsyncIterBreakTest().then(common.mustCall());
+
+async function doAsyncIterReturnTest() {
+  const dir = await fs.promises.opendir(testDir);
+  await (async function() {
+    for await (const dirent of dir) {
+      return;
+    }
+  })();
+
+  await assert.rejects(dir.read(), dirclosedError);
+}
+doAsyncIterReturnTest().then(common.mustCall());
+
+async function doAsyncIterThrowTest() {
+  const dir = await fs.promises.opendir(testDir);
+  try {
+    for await (const dirent of dir) { // eslint-disable-line no-unused-vars
+      throw new Error('oh no');
+    }
+  } catch (err) {
+    if (err.message !== 'oh no') {
+      throw err;
+    }
+  }
+
+  await assert.rejects(dir.read(), dirclosedError);
+}
+doAsyncIterThrowTest().then(common.mustCall());
+
+// Check error thrown on invalid values of bufferSize
+for (const bufferSize of [-1, 0, 0.5, 1.5, Infinity, NaN]) {
+  assert.throws(
+    () => fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize })),
+    {
+      code: 'ERR_OUT_OF_RANGE'
+    });
+}
+for (const bufferSize of ['', '1', null]) {
+  assert.throws(
+    () => fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize })),
+    {
+      code: 'ERR_INVALID_ARG_TYPE'
+    });
+}
+
+// Check that passing a positive integer as bufferSize works
+{
+  const dir = fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize: 1024 }));
+  assertDirent(dir.readSync());
+  dir.close();
+}
+
+// Check that when passing a string instead of function - throw an exception
+async function doAsyncIterInvalidCallbackTest() {
+  const dir = await fs.promises.opendir(testDir);
+  assert.throws(() => dir.close('not function'), invalidCallbackObj);
+  dir.close();
+}
+doAsyncIterInvalidCallbackTest().then(common.mustCall());
+
+// Check first call to close() - should not report an error.
+async function doAsyncIterDirClosedTest() {
+  const dir = await fs.promises.opendir(testDir);
+  await dir.close();
+  await assert.rejects(() => dir.close(), dirclosedError);
+}
+doAsyncIterDirClosedTest().then(common.mustCall());
+
+// Check that readSync() and closeSync() during read() throw exceptions
+async function doConcurrentAsyncAndSyncOps() {
+  const dir = await fs.promises.opendir(testDir);
+  const promise = dir.read();
+
+  assert.throws(() => dir.closeSync(), dirconcurrentError);
+  assert.throws(() => dir.readSync(), dirconcurrentError);
+
+  await promise;
+  dir.closeSync();
+}
+doConcurrentAsyncAndSyncOps().then(common.mustCall());
+
+// Check read throw exceptions on invalid callback
+{
+  const dir = fs.opendirSync(testDir);
+  assert.throws(() => dir.read('INVALID_CALLBACK'), /ERR_INVALID_ARG_TYPE/);
+  dir.close();
+}
+
+// Check that concurrent read() operations don't do weird things.
+async function doConcurrentAsyncOps() {
+  const dir = await fs.promises.opendir(testDir);
+  const promise1 = dir.read();
+  const promise2 = dir.read();
+
+  assertDirent(await promise1);
+  assertDirent(await promise2);
+  dir.closeSync();
+}
+doConcurrentAsyncOps().then(common.mustCall());
+
+// Check that concurrent read() + close() operations don't do weird things.
+async function doConcurrentAsyncMixedOps() {
+  const dir = await fs.promises.opendir(testDir);
+  const promise1 = dir.read();
+  const promise2 = dir.close();
+
+  assertDirent(await promise1);
+  await promise2;
+}
+doConcurrentAsyncMixedOps().then(common.mustCall());
+
+// Check if directory already closed - the callback should pass an error.
+{
+  const dir = fs.opendirSync(testDir);
+  dir.closeSync();
+  dir.close(common.mustCall((error) => {
+    assert.strictEqual(error.code, dirclosedError.code);
+  }));
+}
+
+// Check if directory already closed - throw an promise exception.
+{
+  const dir = fs.opendirSync(testDir);
+  dir.closeSync();
+  assert.rejects(dir.close(), dirclosedError).then(common.mustCall());
+}
diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-pull.js b/test/js/node/test/parallel/test-fs-promises-file-handle-pull.js
new file mode 100644
index 000000000000..3fc531baf713
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-file-handle-pull.js
@@ -0,0 +1,440 @@
+// Flags: --experimental-stream-iter
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const { open } = fs.promises;
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+const { text, bytes } = require('stream/iter');
+
+tmpdir.refresh();
+
+const tmpDir = tmpdir.path;
+
+// =============================================================================
+// Basic pull()
+// =============================================================================
+
+async function testBasicPull() {
+  const filePath = path.join(tmpDir, 'pull-basic.txt');
+  fs.writeFileSync(filePath, 'hello from file');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const readable = fh.pull();
+    const data = await text(readable);
+    assert.strictEqual(data, 'hello from file');
+  } finally {
+    await fh.close();
+  }
+}
+
+async function testPullBinary() {
+  const filePath = path.join(tmpDir, 'pull-binary.bin');
+  const buf = Buffer.alloc(256);
+  for (let i = 0; i < 256; i++) buf[i] = i;
+  fs.writeFileSync(filePath, buf);
+
+  const fh = await open(filePath, 'r');
+  try {
+    const readable = fh.pull();
+    const data = await bytes(readable);
+    assert.strictEqual(data.byteLength, 256);
+    for (let i = 0; i < 256; i++) {
+      assert.strictEqual(data[i], i);
+    }
+  } finally {
+    await fh.close();
+  }
+}
+
+async function testPullEmptyFile() {
+  const filePath = path.join(tmpDir, 'pull-empty.txt');
+  fs.writeFileSync(filePath, '');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const readable = fh.pull();
+    const data = await bytes(readable);
+    assert.strictEqual(data.byteLength, 0);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Large file (multi-chunk)
+// =============================================================================
+
+async function testPullLargeFile() {
+  const filePath = path.join(tmpDir, 'pull-large.bin');
+  // Write 64KB - enough for multiple 16KB read chunks
+  const size = 64 * 1024;
+  const buf = Buffer.alloc(size, 0x42);
+  fs.writeFileSync(filePath, buf);
+
+  const fh = await open(filePath, 'r');
+  try {
+    const readable = fh.pull();
+    const data = await bytes(readable);
+    assert.strictEqual(data.byteLength, size);
+    // Verify content
+    for (let i = 0; i < data.byteLength; i++) {
+      assert.strictEqual(data[i], 0x42);
+    }
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// With transforms
+// =============================================================================
+
+async function testPullWithTransform() {
+  const filePath = path.join(tmpDir, 'pull-transform.txt');
+  fs.writeFileSync(filePath, 'hello');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const upper = (chunks) => {
+      if (chunks === null) return null;
+      return chunks.map((c) => {
+        const str = new TextDecoder().decode(c);
+        return new TextEncoder().encode(str.toUpperCase());
+      });
+    };
+
+    const readable = fh.pull(upper);
+    const data = await text(readable);
+    assert.strictEqual(data, 'HELLO');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// autoClose option
+// =============================================================================
+
+async function testPullAutoClose() {
+  const filePath = path.join(tmpDir, 'pull-autoclose.txt');
+  fs.writeFileSync(filePath, 'auto close data');
+
+  const fh = await open(filePath, 'r');
+  const readable = fh.pull({ autoClose: true });
+  const data = await text(readable);
+  assert.strictEqual(data, 'auto close data');
+
+  // After consuming with autoClose, the file handle should be closed
+  // Trying to read again should throw
+  await assert.rejects(
+    async () => {
+      await fh.stat();
+    },
+    (err) => err.code === 'ERR_INVALID_STATE' || err.code === 'EBADF',
+  );
+}
+
+// =============================================================================
+// Locking
+// =============================================================================
+
+async function testPullLocking() {
+  const filePath = path.join(tmpDir, 'pull-lock.txt');
+  fs.writeFileSync(filePath, 'lock data');
+
+  const fh = await open(filePath, 'r');
+  try {
+    // First pull locks the handle
+    const readable = fh.pull();
+
+    // Second pull while locked should throw
+    assert.throws(
+      () => fh.pull(),
+      { code: 'ERR_INVALID_STATE' },
+    );
+
+    // Consume the first stream to unlock
+    await text(readable);
+
+    // Now it should be usable again
+    const readable2 = fh.pull();
+    const data = await text(readable2);
+    assert.strictEqual(data, '');  // Already read to end
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Closed handle
+// =============================================================================
+
+async function testPullClosedHandle() {
+  const filePath = path.join(tmpDir, 'pull-closed.txt');
+  fs.writeFileSync(filePath, 'data');
+
+  const fh = await open(filePath, 'r');
+  await fh.close();
+
+  assert.throws(
+    () => fh.pull(),
+    { code: 'ERR_INVALID_STATE' },
+  );
+}
+
+// =============================================================================
+// AbortSignal
+// =============================================================================
+
+async function testPullAbortSignal() {
+  const filePath = path.join(tmpDir, 'pull-abort.txt');
+  // Write enough data that we can abort mid-stream
+  fs.writeFileSync(filePath, 'a'.repeat(1024));
+
+  const ac = new AbortController();
+  const fh = await open(filePath, 'r');
+  try {
+    ac.abort();
+    const readable = fh.pull({ signal: ac.signal });
+
+    await assert.rejects(
+      async () => {
+        // eslint-disable-next-line no-unused-vars
+        for await (const _ of readable) {
+          assert.fail('Should not reach here');
+        }
+      },
+      (err) => err.name === 'AbortError',
+    );
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Iterate batches directly
+// =============================================================================
+
+async function testPullIterateBatches() {
+  const filePath = path.join(tmpDir, 'pull-batches.txt');
+  fs.writeFileSync(filePath, 'batch data');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const readable = fh.pull();
+    const batches = [];
+    for await (const batch of readable) {
+      batches.push(batch);
+      // Each batch should be an array of Uint8Array
+      assert.ok(Array.isArray(batch));
+      for (const chunk of batch) {
+        assert.ok(chunk instanceof Uint8Array);
+      }
+    }
+    assert.ok(batches.length > 0);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with start option - read from specific position
+// =============================================================================
+
+async function testPullStart() {
+  const filePath = path.join(tmpDir, 'pull-start.txt');
+  fs.writeFileSync(filePath, 'AAABBBCCC');
+
+  const fh = await open(filePath, 'r');
+  try {
+    // Read from offset 3
+    const data = await text(fh.pull({ start: 3 }));
+    assert.strictEqual(data, 'BBBCCC');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with limit option - read at most N bytes
+// =============================================================================
+
+async function testPullLimit() {
+  const filePath = path.join(tmpDir, 'pull-limit.txt');
+  fs.writeFileSync(filePath, 'Hello, World! Extra data here.');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = await text(fh.pull({ limit: 13 }));
+    assert.strictEqual(data, 'Hello, World!');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with start + limit - read a slice
+// =============================================================================
+
+async function testPullStartAndLimit() {
+  const filePath = path.join(tmpDir, 'pull-start-limit.txt');
+  fs.writeFileSync(filePath, 'AAABBBCCCDDD');
+
+  const fh = await open(filePath, 'r');
+  try {
+    // Read 3 bytes starting at offset 3
+    const data = await text(fh.pull({ start: 3, limit: 3 }));
+    assert.strictEqual(data, 'BBB');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with limit larger than file - reads whole file
+// =============================================================================
+
+async function testPullLimitLargerThanFile() {
+  const filePath = path.join(tmpDir, 'pull-limit-large.txt');
+  fs.writeFileSync(filePath, 'short');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = await text(fh.pull({ limit: 1000000 }));
+    assert.strictEqual(data, 'short');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with limit spanning multiple chunks
+// =============================================================================
+
+async function testPullLimitMultiChunk() {
+  const filePath = path.join(tmpDir, 'pull-limit-multi.bin');
+  // 300KB file - spans multiple 128KB reads
+  const input = Buffer.alloc(300 * 1024, 'x');
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    // Read exactly 200KB from offset 50KB
+    const data = await bytes(fh.pull({ start: 50 * 1024, limit: 200 * 1024 }));
+    assert.strictEqual(data.byteLength, 200 * 1024);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with start + limit + transforms
+// =============================================================================
+
+async function testPullStartLimitWithTransforms() {
+  const filePath = path.join(tmpDir, 'pull-start-limit-transform.txt');
+  fs.writeFileSync(filePath, 'aaabbbcccddd');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const { compressGzip, decompressGzip } = require('zlib/iter');
+    const compressed = fh.pull(compressGzip(), { start: 3, limit: 6 });
+    const decompressed = await text(
+      require('stream/iter').pull(compressed, decompressGzip()));
+    assert.strictEqual(decompressed, 'bbbccc');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pull() with chunkSize option
+// =============================================================================
+
+async function testPullChunkSize() {
+  const filePath = path.join(tmpDir, 'pull-chunksize.bin');
+  // Write 64KB of data
+  const input = Buffer.alloc(64 * 1024, 'z');
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    // Use 16KB chunks - should produce 4 batches
+    let batchCount = 0;
+    for await (const batch of fh.pull({ chunkSize: 16 * 1024 })) {
+      batchCount++;
+      for (const chunk of batch) {
+        assert.ok(chunk.byteLength <= 16 * 1024,
+                  `Chunk ${chunk.byteLength} should be <= 16384`);
+      }
+    }
+    assert.strictEqual(batchCount, 4);
+  } finally {
+    await fh.close();
+  }
+}
+
+async function testPullChunkSizeSmall() {
+  const filePath = path.join(tmpDir, 'pull-chunksize-small.txt');
+  fs.writeFileSync(filePath, 'hello');
+
+  const fh = await open(filePath, 'r');
+  try {
+    // 1-byte chunks
+    let totalBytes = 0;
+    let batchCount = 0;
+    for await (const batch of fh.pull({ chunkSize: 1 })) {
+      batchCount++;
+      for (const chunk of batch) totalBytes += chunk.byteLength;
+    }
+    assert.strictEqual(totalBytes, 5);
+    assert.strictEqual(batchCount, 5);
+  } finally {
+    await fh.close();
+  }
+}
+
+async function testPullSyncArgumentValidation() {
+  const filePath = path.join(tmpDir, 'pull-arg-validation.txt');
+  fs.writeFileSync(filePath, 'data');
+
+  const fh = await open(filePath, 'r');
+  try {
+    assert.throws(() => fh.pull({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pull({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pull({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pull({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pull({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pull({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.pull({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.pull({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+  } finally {
+    await fh.close();
+  }
+}
+
+Promise.all([
+  testBasicPull(),
+  testPullBinary(),
+  testPullEmptyFile(),
+  testPullLargeFile(),
+  testPullWithTransform(),
+  testPullAutoClose(),
+  testPullLocking(),
+  testPullClosedHandle(),
+  testPullAbortSignal(),
+  testPullIterateBatches(),
+  testPullStart(),
+  testPullLimit(),
+  testPullStartAndLimit(),
+  testPullLimitLargerThanFile(),
+  testPullLimitMultiChunk(),
+  testPullStartLimitWithTransforms(),
+  testPullChunkSize(),
+  testPullChunkSizeSmall(),
+  testPullSyncArgumentValidation(),
+]).then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-pullsync.js b/test/js/node/test/parallel/test-fs-promises-file-handle-pullsync.js
new file mode 100644
index 000000000000..20c429972573
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-file-handle-pullsync.js
@@ -0,0 +1,498 @@
+// Flags: --experimental-stream-iter
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const { open } = fs.promises;
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+const {
+  textSync,
+  bytesSync,
+  pipeToSync,
+  pullSync,
+} = require('stream/iter');
+const {
+  compressGzipSync,
+  decompressGzipSync,
+} = require('zlib/iter');
+
+tmpdir.refresh();
+
+const tmpDir = tmpdir.path;
+
+// =============================================================================
+// Basic pullSync()
+// =============================================================================
+
+async function testBasicPullSync() {
+  const filePath = path.join(tmpDir, 'pullsync-basic.txt');
+  fs.writeFileSync(filePath, 'hello from sync file read');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync());
+    assert.strictEqual(data, 'hello from sync file read');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Large file (multi-chunk)
+// =============================================================================
+
+async function testLargeFile() {
+  const filePath = path.join(tmpDir, 'pullsync-large.txt');
+  const input = 'sync large data test. '.repeat(10000);
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync());
+    assert.strictEqual(data, input);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Binary data round-trip
+// =============================================================================
+
+async function testBinaryData() {
+  const filePath = path.join(tmpDir, 'pullsync-binary.bin');
+  const input = Buffer.alloc(200000);
+  for (let i = 0; i < input.length; i++) input[i] = i & 0xff;
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = bytesSync(fh.pullSync());
+    assert.deepStrictEqual(Buffer.from(data), input);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync with sync compression transform round-trip
+// =============================================================================
+
+async function testPullSyncWithCompression() {
+  const filePath = path.join(tmpDir, 'pullsync-compress-src.txt');
+  const dstPath = path.join(tmpDir, 'pullsync-compress-dst.gz');
+  const input = 'compress via sync pullSync. '.repeat(1000);
+  fs.writeFileSync(filePath, input);
+
+  // Compress: pullSync -> compressGzipSync -> write to file
+  const srcFh = await open(filePath, 'r');
+  const dstFh = await open(dstPath, 'w');
+  try {
+    const w = dstFh.writer();
+    pipeToSync(srcFh.pullSync(compressGzipSync()), w);
+  } finally {
+    await srcFh.close();
+    await dstFh.close();
+  }
+
+  // Verify compressed file is smaller
+  const compressedSize = fs.statSync(dstPath).size;
+  assert.ok(compressedSize < Buffer.byteLength(input),
+            `Compressed ${compressedSize} should be < original ` +
+            `${Buffer.byteLength(input)}`);
+
+  // Decompress and verify
+  const readFh = await open(dstPath, 'r');
+  try {
+    const result = textSync(readFh.pullSync(decompressGzipSync()));
+    assert.strictEqual(result, input);
+  } finally {
+    await readFh.close();
+  }
+}
+
+// =============================================================================
+// pullSync with stateless transform
+// =============================================================================
+
+async function testPullSyncWithStatelessTransform() {
+  const filePath = path.join(tmpDir, 'pullsync-upper.txt');
+  fs.writeFileSync(filePath, 'hello world');
+
+  const upper = (chunks) => {
+    if (chunks === null) return null;
+    const out = new Array(chunks.length);
+    for (let j = 0; j < chunks.length; j++) {
+      const src = chunks[j];
+      const buf = Buffer.allocUnsafe(src.length);
+      for (let i = 0; i < src.length; i++) {
+        const b = src[i];
+        buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
+      }
+      out[j] = buf;
+    }
+    return out;
+  };
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync(upper));
+    assert.strictEqual(data, 'HELLO WORLD');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync with mixed stateless + stateful transforms
+// =============================================================================
+
+async function testPullSyncMixedTransforms() {
+  const filePath = path.join(tmpDir, 'pullsync-mixed.txt');
+  const input = 'mixed transform test '.repeat(500);
+  fs.writeFileSync(filePath, input);
+
+  const upper = (chunks) => {
+    if (chunks === null) return null;
+    const out = new Array(chunks.length);
+    for (let j = 0; j < chunks.length; j++) {
+      const src = chunks[j];
+      const buf = Buffer.allocUnsafe(src.length);
+      for (let i = 0; i < src.length; i++) {
+        const b = src[i];
+        buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
+      }
+      out[j] = buf;
+    }
+    return out;
+  };
+
+  const fh = await open(filePath, 'r');
+  try {
+    // Upper + compress + decompress
+    const data = textSync(
+      pullSync(fh.pullSync(upper, compressGzipSync()), decompressGzipSync()),
+    );
+    assert.strictEqual(data, input.toUpperCase());
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// autoClose: true - handle closed after iteration completes
+// =============================================================================
+
+async function testAutoClose() {
+  const filePath = path.join(tmpDir, 'pullsync-autoclose.txt');
+  fs.writeFileSync(filePath, 'auto close test');
+
+  const fh = await open(filePath, 'r');
+  const data = textSync(fh.pullSync({ autoClose: true }));
+  assert.strictEqual(data, 'auto close test');
+
+  // Handle should be closed
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+}
+
+// =============================================================================
+// autoClose: true with early break
+// =============================================================================
+
+async function testAutoCloseEarlyBreak() {
+  const filePath = path.join(tmpDir, 'pullsync-autoclose-break.txt');
+  fs.writeFileSync(filePath, 'x'.repeat(1000000));
+
+  const fh = await open(filePath, 'r');
+  // eslint-disable-next-line no-unused-vars
+  for (const batch of fh.pullSync({ autoClose: true })) {
+    break; // Early exit
+  }
+
+  // Handle should be closed by autoClose
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+}
+
+// =============================================================================
+// autoClose: false (default) - handle stays open
+// =============================================================================
+
+async function testNoAutoClose() {
+  const filePath = path.join(tmpDir, 'pullsync-no-autoclose.txt');
+  fs.writeFileSync(filePath, 'still open');
+
+  const fh = await open(filePath, 'r');
+  const data = textSync(fh.pullSync());
+  assert.strictEqual(data, 'still open');
+
+  // Handle should still be open and reusable
+  const stat = await fh.stat();
+  assert.ok(stat.size > 0);
+  await fh.close();
+}
+
+// =============================================================================
+// Lock semantics - pullSync locks the handle
+// =============================================================================
+
+async function testLocked() {
+  const filePath = path.join(tmpDir, 'pullsync-locked.txt');
+  fs.writeFileSync(filePath, 'lock test');
+
+  const fh = await open(filePath, 'r');
+  const iter = fh.pullSync()[Symbol.iterator]();
+  iter.next(); // Start iteration, handle is locked
+
+  assert.throws(() => fh.pullSync(), {
+    code: 'ERR_INVALID_STATE',
+  });
+
+  assert.throws(() => fh.pull(), {
+    code: 'ERR_INVALID_STATE',
+  });
+
+  // Finish iteration to unlock
+  while (!iter.next().done) { /* drain */ }
+  await fh.close();
+}
+
+// =============================================================================
+// Empty file
+// =============================================================================
+
+async function testEmptyFile() {
+  const filePath = path.join(tmpDir, 'pullsync-empty.txt');
+  fs.writeFileSync(filePath, '');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync());
+    assert.strictEqual(data, '');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pipeToSync: file-to-file sync pipeline
+// =============================================================================
+
+async function testPipeToSync() {
+  const srcPath = path.join(tmpDir, 'pullsync-pipeto-src.txt');
+  const dstPath = path.join(tmpDir, 'pullsync-pipeto-dst.txt');
+  const input = 'pipeToSync test data '.repeat(200);
+  fs.writeFileSync(srcPath, input);
+
+  const srcFh = await open(srcPath, 'r');
+  const dstFh = await open(dstPath, 'w');
+  try {
+    const w = dstFh.writer();
+    pipeToSync(srcFh.pullSync(), w);
+  } finally {
+    await srcFh.close();
+    await dstFh.close();
+  }
+
+  assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), input);
+}
+
+// =============================================================================
+// pullSync() with start option
+// =============================================================================
+
+async function testPullSyncStart() {
+  const filePath = path.join(tmpDir, 'pullsync-start.txt');
+  fs.writeFileSync(filePath, 'AAABBBCCC');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync({ start: 3 }));
+    assert.strictEqual(data, 'BBBCCC');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync() with limit option
+// =============================================================================
+
+async function testPullSyncLimit() {
+  const filePath = path.join(tmpDir, 'pullsync-limit.txt');
+  fs.writeFileSync(filePath, 'Hello, World! Extra data here.');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync({ limit: 13 }));
+    assert.strictEqual(data, 'Hello, World!');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync() with start + limit
+// =============================================================================
+
+async function testPullSyncStartAndLimit() {
+  const filePath = path.join(tmpDir, 'pullsync-start-limit.txt');
+  fs.writeFileSync(filePath, 'AAABBBCCCDDD');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = textSync(fh.pullSync({ start: 3, limit: 3 }));
+    assert.strictEqual(data, 'BBB');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync() with limit spanning multiple chunks
+// =============================================================================
+
+async function testPullSyncLimitMultiChunk() {
+  const filePath = path.join(tmpDir, 'pullsync-limit-multi.bin');
+  const input = Buffer.alloc(300 * 1024, 'x');
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    const data = bytesSync(fh.pullSync({ start: 50 * 1024, limit: 200 * 1024 }));
+    assert.strictEqual(data.byteLength, 200 * 1024);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync() with start + limit + compression transform
+// =============================================================================
+
+async function testPullSyncStartLimitWithTransforms() {
+  const filePath = path.join(tmpDir, 'pullsync-start-limit-transform.txt');
+  fs.writeFileSync(filePath, 'aaabbbcccddd');
+
+  const fh = await open(filePath, 'r');
+  try {
+    const compressed = fh.pullSync(compressGzipSync(),
+                                   { start: 3, limit: 6 });
+    const decompressed = textSync(pullSync(compressed, decompressGzipSync()));
+    assert.strictEqual(decompressed, 'bbbccc');
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// pullSync() with start + autoClose
+// =============================================================================
+
+async function testPullSyncStartAutoClose() {
+  const filePath = path.join(tmpDir, 'pullsync-start-autoclose.txt');
+  fs.writeFileSync(filePath, 'AAABBBCCC');
+
+  const fh = await open(filePath, 'r');
+  const data = textSync(fh.pullSync({ start: 3, autoClose: true }));
+  assert.strictEqual(data, 'BBBCCC');
+
+  // Handle should be closed
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+}
+
+// =============================================================================
+// pullSync() with chunkSize option
+// =============================================================================
+
+async function testPullSyncChunkSize() {
+  const filePath = path.join(tmpDir, 'pullsync-chunksize.bin');
+  const input = Buffer.alloc(64 * 1024, 'z');
+  fs.writeFileSync(filePath, input);
+
+  const fh = await open(filePath, 'r');
+  try {
+    let batchCount = 0;
+    for (const batch of fh.pullSync({ chunkSize: 16 * 1024 })) {
+      batchCount++;
+      for (const chunk of batch) {
+        assert.ok(chunk.byteLength <= 16 * 1024,
+                  `Chunk ${chunk.byteLength} should be <= 16384`);
+      }
+    }
+    assert.strictEqual(batchCount, 4);
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// writer() with chunkSize option (sync write threshold)
+// =============================================================================
+
+async function testWriterChunkSize() {
+  const filePath = path.join(tmpDir, 'pullsync-writer-chunksize.txt');
+  const fh = await open(filePath, 'w');
+  // Set chunkSize to 1024 - writes larger than this should fall back to async
+  const w = fh.writer({ chunkSize: 1024 });
+
+  // Small write should succeed sync
+  assert.strictEqual(w.writeSync(Buffer.alloc(512, 'a')), true);
+
+  // Write larger than chunkSize should return false
+  assert.strictEqual(w.writeSync(Buffer.alloc(2048, 'b')), false);
+
+  await w.end();
+  await fh.close();
+}
+
+// =============================================================================
+// Argument validation
+// =============================================================================
+
+async function testPullArgumentValidation() {
+  const filePath = path.join(tmpDir, 'pull-arg-validation.txt');
+  fs.writeFileSync(filePath, 'data');
+
+  const fh = await open(filePath, 'r');
+  try {
+    assert.throws(() => fh.pullSync({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pullSync({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pullSync({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pullSync({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.pullSync({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.pullSync({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.pullSync({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Run all tests
+// =============================================================================
+
+Promise.all([
+  testBasicPullSync(),
+  testLargeFile(),
+  testBinaryData(),
+  testPullSyncWithCompression(),
+  testPullSyncWithStatelessTransform(),
+  testPullSyncMixedTransforms(),
+  testAutoClose(),
+  testAutoCloseEarlyBreak(),
+  testNoAutoClose(),
+  testLocked(),
+  testEmptyFile(),
+  testPipeToSync(),
+  testPullSyncStart(),
+  testPullSyncLimit(),
+  testPullSyncStartAndLimit(),
+  testPullSyncLimitMultiChunk(),
+  testPullSyncStartLimitWithTransforms(),
+  testPullSyncStartAutoClose(),
+  testPullSyncChunkSize(),
+  testWriterChunkSize(),
+  testPullArgumentValidation(),
+]).then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js b/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js
new file mode 100644
index 000000000000..93669c279f83
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js
@@ -0,0 +1,54 @@
+'use strict';
+const common = require('../common');
+const fs = require('fs');
+const assert = require('assert');
+const tmpdir = require('../common/tmpdir');
+const file = tmpdir.resolve('read_stream_filehandle_worker.txt');
+const input = 'hello world';
+const { Worker, isMainThread, workerData } = require('worker_threads');
+
+if (isMainThread || !workerData) {
+  tmpdir.refresh();
+  fs.writeFileSync(file, input);
+
+  fs.promises.open(file, 'r').then((handle) => {
+    handle.on('close', common.mustNotCall());
+    new Worker(__filename, {
+      workerData: { handle },
+      transferList: [handle]
+    });
+  }).then(common.mustCall());
+  fs.promises.open(file, 'r').then(async (handle) => {
+    try {
+      fs.createReadStream(null, { fd: handle });
+      assert.throws(() => {
+        new Worker(__filename, {
+          workerData: { handle },
+          transferList: [handle]
+        });
+      }, {
+        code: 25,
+        name: 'DataCloneError',
+      });
+    } finally {
+      await handle.close();
+    }
+  }).then(common.mustCall());
+} else {
+  let output = '';
+
+  const handle = workerData.handle;
+  handle.on('close', common.mustCall());
+  const stream = fs.createReadStream(null, { fd: handle });
+
+  stream.on('data', common.mustCallAtLeast((data) => {
+    output += data;
+  }));
+
+  stream.on('end', common.mustCall(() => {
+    handle.close();
+    assert.strictEqual(output, input);
+  }));
+
+  stream.on('close', common.mustCall());
+}
diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-writer.js b/test/js/node/test/parallel/test-fs-promises-file-handle-writer.js
new file mode 100644
index 000000000000..ff90716400ef
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-file-handle-writer.js
@@ -0,0 +1,1117 @@
+// Flags: --experimental-stream-iter
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const { open } = fs.promises;
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+const {
+  pipeTo, text,
+} = require('stream/iter');
+const {
+  compressGzip, decompressGzip,
+} = require('zlib/iter');
+
+tmpdir.refresh();
+
+const tmpDir = tmpdir.path;
+
+// =============================================================================
+// Basic write()
+// =============================================================================
+
+async function testBasicWrite() {
+  const filePath = path.join(tmpDir, 'writer-basic.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.write(Buffer.from('Hello '));
+  await w.write(Buffer.from('World!'));
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 12);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!');
+}
+
+// =============================================================================
+// Basic writev()
+// =============================================================================
+
+async function testBasicWritev() {
+  const filePath = path.join(tmpDir, 'writer-writev.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.writev([
+    Buffer.from('aaa'),
+    Buffer.from('bbb'),
+    Buffer.from('ccc'),
+  ]);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 9);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc');
+}
+
+// =============================================================================
+// Mixed write() and writev()
+// =============================================================================
+
+async function testMixedWriteAndWritev() {
+  const filePath = path.join(tmpDir, 'writer-mixed.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.write(Buffer.from('head-'));
+  await w.writev([Buffer.from('mid1-'), Buffer.from('mid2-')]);
+  await w.write(Buffer.from('tail'));
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 19);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'head-mid1-mid2-tail');
+}
+
+// =============================================================================
+// end() returns totalBytesWritten
+// =============================================================================
+
+async function testEndReturnsTotalBytes() {
+  const filePath = path.join(tmpDir, 'writer-totalbytes.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  // Write some data in various sizes
+  const sizes = [100, 200, 300, 400, 500];
+  let expected = 0;
+  for (const size of sizes) {
+    await w.write(Buffer.alloc(size, 0x41));
+    expected += size;
+  }
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, expected);
+  assert.strictEqual(totalBytes, 1500);
+  assert.strictEqual(fs.statSync(filePath).size, 1500);
+}
+
+// =============================================================================
+// autoClose: true - handle closed after end()
+// =============================================================================
+
+async function testAutoCloseOnEnd() {
+  const filePath = path.join(tmpDir, 'writer-autoclose-end.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ autoClose: true });
+  await w.write(Buffer.from('auto close test'));
+  await w.end();
+
+  // Handle should be closed
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'auto close test');
+}
+
+// =============================================================================
+// autoClose: true - handle closed after fail()
+// =============================================================================
+
+async function testAutoCloseOnFail() {
+  const filePath = path.join(tmpDir, 'writer-autoclose-fail.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ autoClose: true });
+  await w.write(Buffer.from('partial'));
+  w.fail(new Error('test fail'));
+
+  // Handle should be closed
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+  // Partial data should still be on disk (fail doesn't truncate)
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'partial');
+}
+
+// =============================================================================
+// start option - write at specified offset
+// =============================================================================
+
+async function testStartOption() {
+  const filePath = path.join(tmpDir, 'writer-start.txt');
+  // Pre-fill with 10 A's
+  fs.writeFileSync(filePath, 'AAAAAAAAAA');
+
+  const fh = await open(filePath, 'r+');
+  const w = fh.writer({ start: 3 });
+  await w.write(Buffer.from('BBB'));
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'AAABBBAAAA');
+}
+
+// =============================================================================
+// start option - sequential writes advance position
+// =============================================================================
+
+async function testStartSequentialPosition() {
+  const filePath = path.join(tmpDir, 'writer-start-seq.txt');
+  fs.writeFileSync(filePath, 'XXXXXXXXXX');
+
+  const fh = await open(filePath, 'r+');
+  const w = fh.writer({ start: 2 });
+  await w.write(Buffer.from('AA'));
+  await w.write(Buffer.from('BB'));
+  await w.writev([Buffer.from('C'), Buffer.from('D')]);
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'XXAABBCDXX');
+}
+
+// =============================================================================
+// Locked state - can't create second writer while active
+// =============================================================================
+
+async function testLockedState() {
+  const filePath = path.join(tmpDir, 'writer-locked.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  assert.throws(() => fh.writer(), {
+    name: 'Error',
+    message: /locked/,
+  });
+
+  // Also can't pull while writer is active
+  assert.throws(() => fh.pull(), {
+    name: 'Error',
+    message: /locked/,
+  });
+
+  await w.end();
+  await fh.close();
+}
+
+// =============================================================================
+// Unlock after end - handle reusable
+// =============================================================================
+
+async function testUnlockAfterEnd() {
+  const filePath = path.join(tmpDir, 'writer-unlock.txt');
+  const fh = await open(filePath, 'w');
+
+  const w1 = fh.writer();
+  await w1.write(Buffer.from('first'));
+  await w1.end();
+
+  // Should work - handle is unlocked
+  const w2 = fh.writer();
+  await w2.write(Buffer.from(' second'));
+  await w2.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'first second');
+}
+
+// =============================================================================
+// Unlock after fail - handle reusable
+// =============================================================================
+
+async function testUnlockAfterFail() {
+  const filePath = path.join(tmpDir, 'writer-unlock-fail.txt');
+  const fh = await open(filePath, 'w');
+
+  const w1 = fh.writer();
+  await w1.write(Buffer.from('failed'));
+  await w1.fail(new Error('test'));
+
+  // Should work - handle is unlocked
+  const w2 = fh.writer();
+  await w2.write(Buffer.from('recovered'));
+  await w2.end();
+  await fh.close();
+
+  // 'recovered' is appended after 'failed' at current file offset
+  const content = fs.readFileSync(filePath, 'utf8');
+  assert.ok(content.startsWith('failed'));
+  assert.ok(content.includes('recovered'));
+}
+
+// =============================================================================
+// Write after end/fail rejects
+// =============================================================================
+
+async function testWriteAfterEndRejects() {
+  const filePath = path.join(tmpDir, 'writer-closed.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.write(Buffer.from('data'));
+  await w.end();
+
+  await assert.rejects(w.write(Buffer.from('more')), {
+    name: 'TypeError',
+    message: /closed/,
+  });
+  await assert.rejects(w.writev([Buffer.from('more')]), {
+    name: 'TypeError',
+    message: /closed/,
+  });
+
+  await fh.close();
+}
+
+// =============================================================================
+// Closed handle - writer() throws
+// =============================================================================
+
+async function testClosedHandle() {
+  const filePath = path.join(tmpDir, 'writer-closed-handle.txt');
+  const fh = await open(filePath, 'w');
+  await fh.close();
+
+  assert.throws(() => fh.writer(), {
+    name: 'Error',
+    message: /closed/,
+  });
+}
+
+// =============================================================================
+// pipeTo() integration - pipe source through writer
+// =============================================================================
+
+async function testPipeToIntegration() {
+  const srcPath = path.join(tmpDir, 'writer-pipeto-src.txt');
+  const dstPath = path.join(tmpDir, 'writer-pipeto-dst.txt');
+  const data = 'The quick brown fox jumps over the lazy dog.\n'.repeat(500);
+  fs.writeFileSync(srcPath, data);
+
+  const rfh = await open(srcPath, 'r');
+  const wfh = await open(dstPath, 'w');
+  const w = wfh.writer();
+
+  const totalBytes = await pipeTo(rfh.pull(), w);
+
+  await rfh.close();
+  await wfh.close();
+
+  assert.strictEqual(totalBytes, Buffer.byteLength(data));
+  assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), data);
+}
+
+// =============================================================================
+// pipeTo() with transforms - uppercase through writer
+// =============================================================================
+
+async function testPipeToWithTransform() {
+  const srcPath = path.join(tmpDir, 'writer-transform-src.txt');
+  const dstPath = path.join(tmpDir, 'writer-transform-dst.txt');
+  const data = 'hello world from transforms test\n'.repeat(200);
+  fs.writeFileSync(srcPath, data);
+
+  function uppercase(chunks) {
+    if (chunks === null) return null;
+    const out = new Array(chunks.length);
+    for (let i = 0; i < chunks.length; i++) {
+      const src = chunks[i];
+      const buf = Buffer.allocUnsafe(src.length);
+      for (let j = 0; j < src.length; j++) {
+        const b = src[j];
+        buf[j] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
+      }
+      out[i] = buf;
+    }
+    return out;
+  }
+
+  const rfh = await open(srcPath, 'r');
+  const wfh = await open(dstPath, 'w');
+  const w = wfh.writer();
+
+  await pipeTo(rfh.pull(), uppercase, w);
+
+  await rfh.close();
+  await wfh.close();
+
+  assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), data.toUpperCase());
+}
+
+// =============================================================================
+// Round-trip: pull → compress → writer, pull → decompress → verify
+// =============================================================================
+
+async function testCompressRoundTrip() {
+  const srcPath = path.join(tmpDir, 'writer-rt-src.txt');
+  const gzPath = path.join(tmpDir, 'writer-rt.gz');
+  const original = 'Round trip compression test data. '.repeat(2000);
+  fs.writeFileSync(srcPath, original);
+
+  // Compress: pull → gzip → writer
+  {
+    const rfh = await open(srcPath, 'r');
+    const wfh = await open(gzPath, 'w');
+    const w = wfh.writer({ autoClose: true });
+    await pipeTo(rfh.pull(), compressGzip(), w);
+    await rfh.close();
+  }
+
+  // Verify compressed file is smaller
+  const compressedSize = fs.statSync(gzPath).size;
+  assert.ok(compressedSize < Buffer.byteLength(original),
+            `Compressed ${compressedSize} should be < original ${Buffer.byteLength(original)}`);
+
+  // Decompress: pull → gunzip → text → verify
+  {
+    const rfh = await open(gzPath, 'r');
+    const result = await text(rfh.pull(decompressGzip()));
+    await rfh.close();
+    assert.strictEqual(result, original);
+  }
+}
+
+// =============================================================================
+// Large file write - write 1MB in 64KB chunks
+// =============================================================================
+
+async function testLargeFileWrite() {
+  const filePath = path.join(tmpDir, 'writer-large.bin');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  const chunkSize = 65536;
+  const totalSize = 1024 * 1024; // 1MB
+  const chunk = Buffer.alloc(chunkSize, 0x42);
+  let written = 0;
+
+  while (written < totalSize) {
+    await w.write(chunk);
+    written += chunkSize;
+  }
+
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, totalSize);
+  assert.strictEqual(fs.statSync(filePath).size, totalSize);
+
+  // Verify content
+  const data = fs.readFileSync(filePath);
+  for (let i = 0; i < data.length; i++) {
+    if (data[i] !== 0x42) {
+      assert.fail(`Byte at offset ${i} is ${data[i]}, expected 0x42`);
+    }
+  }
+}
+
+// =============================================================================
+// Symbol.asyncDispose - await using
+// =============================================================================
+
+async function testAsyncDispose() {
+  const filePath = path.join(tmpDir, 'writer-async-dispose.txt');
+  {
+    await using fh = await open(filePath, 'w');
+    await using w = fh.writer({ autoClose: true });
+    await w.write(Buffer.from('async dispose'));
+  }
+  // Both writer and file handle should be cleaned up
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'async dispose');
+
+  // Verify the handle is actually closed by trying to open a new one
+  // (if the old one were still open with a write lock on some OSes,
+  // this could fail - but it should succeed).
+  const fh2 = await open(filePath, 'r');
+  await fh2.close();
+}
+
+// =============================================================================
+// Symbol.asyncDispose - cleanup on error (await using unwinds)
+// =============================================================================
+
+async function testAsyncDisposeOnError() {
+  const filePath = path.join(tmpDir, 'writer-dispose-error.txt');
+  const fh = await open(filePath, 'w');
+
+  try {
+    await using w = fh.writer();
+    await w.write(Buffer.from('before error'));
+    throw new Error('intentional');
+  } catch (e) {
+    assert.strictEqual(e.message, 'intentional');
+  }
+
+  // If asyncDispose ran, the handle should be unlocked and reusable
+  const w2 = fh.writer();
+  await w2.write(Buffer.from('after error'));
+  await w2.end();
+  await fh.close();
+
+  const content = fs.readFileSync(filePath, 'utf8');
+  assert.ok(content.includes('after error'),
+            `Expected 'after error' in ${JSON.stringify(content)}`);
+}
+
+// =============================================================================
+// Pre-aborted signal rejects write/writev/end
+// =============================================================================
+
+async function testWriteWithAbortedSignalRejects() {
+  const filePath = path.join(tmpDir, 'writer-signal-write.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await assert.rejects(
+    w.write(Buffer.from('data'), { signal: AbortSignal.abort() }),
+    { name: 'AbortError' },
+  );
+
+  // Writer should still be usable after a signal rejection
+  await w.write(Buffer.from('ok'));
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'ok');
+}
+
+async function testWritevWithAbortedSignalRejects() {
+  const filePath = path.join(tmpDir, 'writer-signal-writev.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await assert.rejects(
+    w.writev([Buffer.from('a'), Buffer.from('b')], { signal: AbortSignal.abort() }),
+    { name: 'AbortError' },
+  );
+
+  await w.writev([Buffer.from('ok')]);
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'ok');
+}
+
+async function testEndWithAbortedSignalRejects() {
+  const filePath = path.join(tmpDir, 'writer-signal-end.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await w.write(Buffer.from('data'));
+
+  await assert.rejects(
+    w.end({ signal: AbortSignal.abort() }),
+    { name: 'AbortError' },
+  );
+
+  // end() was rejected so writer is still open - end it cleanly
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 4);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'data');
+}
+
+// =============================================================================
+// write() with string input (UTF-8 encoding)
+// =============================================================================
+
+async function testWriteString() {
+  const filePath = path.join(tmpDir, 'writer-string.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.write('Hello ');
+  await w.write('World!');
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 12);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!');
+}
+
+// =============================================================================
+// write() with string containing multi-byte UTF-8 characters
+// =============================================================================
+
+async function testWriteStringMultibyte() {
+  const filePath = path.join(tmpDir, 'writer-string-multibyte.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  const input = 'café ☕ 日本語';
+  await w.write(input);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  const expected = Buffer.from(input, 'utf8');
+  assert.strictEqual(totalBytes, expected.byteLength);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), input);
+}
+
+// =============================================================================
+// writev() with string chunks (UTF-8 encoding)
+// =============================================================================
+
+async function testWritevStrings() {
+  const filePath = path.join(tmpDir, 'writer-writev-strings.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.writev(['aaa', 'bbb', 'ccc']);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 9);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc');
+}
+
+// =============================================================================
+// writev() with mixed string and Uint8Array chunks
+// =============================================================================
+
+async function testWritevMixed() {
+  const filePath = path.join(tmpDir, 'writer-writev-mixed.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  await w.writev(['hello', Buffer.from(' '), 'world']);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 11);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'hello world');
+}
+
+// =============================================================================
+// Symbol.dispose calls fail()
+// =============================================================================
+
+async function testSyncDispose() {
+  const filePath = path.join(tmpDir, 'writer-sync-dispose.txt');
+  const fh = await open(filePath, 'w');
+
+  {
+    using w = fh.writer();
+    await w.write(Buffer.from('before dispose'));
+  }
+  // Symbol.dispose calls fail(), which unlocks the handle.
+  // The handle should be reusable.
+  const w2 = fh.writer();
+  await w2.write(Buffer.from('after dispose'));
+  await w2.end();
+  await fh.close();
+
+  const content = fs.readFileSync(filePath, 'utf8');
+  assert.ok(content.includes('after dispose'),
+            `Expected 'after dispose' in ${JSON.stringify(content)}`);
+}
+
+// =============================================================================
+// Symbol.dispose on error unwind
+// =============================================================================
+
+async function testSyncDisposeOnError() {
+  const filePath = path.join(tmpDir, 'writer-sync-dispose-error.txt');
+  const fh = await open(filePath, 'w');
+
+  try {
+    using w = fh.writer();
+    await w.write(Buffer.from('data'));
+    throw new Error('intentional');
+  } catch (e) {
+    assert.strictEqual(e.message, 'intentional');
+  }
+
+  // Handle should be unlocked and reusable after sync dispose
+  const w2 = fh.writer();
+  await w2.write(Buffer.from('recovered'));
+  await w2.end();
+  await fh.close();
+
+  const content = fs.readFileSync(filePath, 'utf8');
+  assert.ok(content.includes('recovered'),
+            `Expected 'recovered' in ${JSON.stringify(content)}`);
+}
+
+// =============================================================================
+// writeSync() basic
+// =============================================================================
+
+async function testWriteSyncBasic() {
+  const filePath = path.join(tmpDir, 'writer-writesync-basic.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  assert.strictEqual(w.writeSync('Hello '), true);
+  assert.strictEqual(w.writeSync(Buffer.from('World!')), true);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 12);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!');
+}
+
+// =============================================================================
+// writevSync() basic
+// =============================================================================
+
+async function testWritevSyncBasic() {
+  const filePath = path.join(tmpDir, 'writer-writevsync-basic.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  assert.strictEqual(w.writevSync(['aaa', Buffer.from('bbb'), 'ccc']), true);
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 9);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc');
+}
+
+// =============================================================================
+// writeSync() returns false for large chunks
+// =============================================================================
+
+async function testWriteSyncLargeChunk() {
+  const filePath = path.join(tmpDir, 'writer-writesync-large.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  // Chunk larger than 131072 should return false
+  const bigChunk = Buffer.alloc(131073, 'x');
+  assert.strictEqual(w.writeSync(bigChunk), false);
+
+  // Chunk at exactly 131072 should succeed
+  const exactChunk = Buffer.alloc(131072, 'y');
+  assert.strictEqual(w.writeSync(exactChunk), true);
+
+  await w.end();
+  await fh.close();
+
+  // Only the exact chunk should have been written
+  const content = fs.readFileSync(filePath);
+  assert.strictEqual(content.length, 131072);
+}
+
+// =============================================================================
+// writeSync() returns false when async op is in flight
+// =============================================================================
+
+async function testWriteSyncReturnsFalseDuringAsync() {
+  const filePath = path.join(tmpDir, 'writer-writesync-async.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  // Start an async write but don't await yet
+  const p = w.write(Buffer.from('async'));
+
+  // Sync write should return false because async is in flight
+  assert.strictEqual(w.writeSync(Buffer.from('sync')), false);
+
+  await p;
+
+  // After async completes, sync should work again
+  assert.strictEqual(w.writeSync(Buffer.from(' then sync')), true);
+
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'async then sync');
+}
+
+// =============================================================================
+// writeSync() returns false on closed/errored writer
+// =============================================================================
+
+async function testWriteSyncClosedErrored() {
+  const filePath = path.join(tmpDir, 'writer-writesync-closed.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await w.end();
+
+  // Should return false after end()
+  assert.strictEqual(w.writeSync(Buffer.from('data')), false);
+  await fh.close();
+
+  // Test errored state
+  const fh2 = await open(filePath, 'w');
+  const w2 = fh2.writer();
+  w2.fail(new Error('test'));
+  assert.strictEqual(w2.writeSync(Buffer.from('data')), false);
+  await fh2.close();
+}
+
+// =============================================================================
+// endSync() basic
+// =============================================================================
+
+async function testEndSyncBasic() {
+  const filePath = path.join(tmpDir, 'writer-endsync-basic.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  w.writeSync(Buffer.from('hello'));
+  const totalBytes = w.endSync();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 5);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'hello');
+}
+
+// =============================================================================
+// endSync() returns -1 when async op is in flight
+// =============================================================================
+
+async function testEndSyncReturnsFalseDuringAsync() {
+  const filePath = path.join(tmpDir, 'writer-endsync-async.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  const p = w.write(Buffer.from('data'));
+  assert.strictEqual(w.endSync(), -1);
+
+  await p;
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 4);
+}
+
+// =============================================================================
+// endSync() idempotent on closed writer
+// =============================================================================
+
+async function testEndSyncIdempotent() {
+  const filePath = path.join(tmpDir, 'writer-endsync-idempotent.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  w.writeSync(Buffer.from('data'));
+  const first = w.endSync();
+  const second = w.endSync();
+
+  assert.strictEqual(first, 4);
+  assert.strictEqual(second, 4);  // Idempotent
+  await fh.close();
+}
+
+// =============================================================================
+// endSync() with autoClose fires handle.close()
+// =============================================================================
+
+async function testEndSyncAutoClose() {
+  const filePath = path.join(tmpDir, 'writer-endsync-autoclose.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ autoClose: true });
+
+  w.writeSync(Buffer.from('auto'));
+  const totalBytes = w.endSync();
+
+  assert.strictEqual(totalBytes, 4);
+
+  // Handle should be closed synchronously
+  await assert.rejects(fh.stat(), { code: 'EBADF' });
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'auto');
+}
+
+// =============================================================================
+// Full sync pipeline: writeSync + endSync (no async at all)
+// =============================================================================
+
+async function testFullSyncPipeline() {
+  const filePath = path.join(tmpDir, 'writer-full-sync.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  // Entirely synchronous write pipeline
+  w.writeSync('line 1\n');
+  w.writeSync('line 2\n');
+  w.writevSync(['line 3\n', 'line 4\n']);
+  const totalBytes = w.endSync();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 28);
+  assert.strictEqual(
+    fs.readFileSync(filePath, 'utf8'),
+    'line 1\nline 2\nline 3\nline 4\n',
+  );
+}
+
+// =============================================================================
+// end() rejects on errored writer
+// =============================================================================
+
+async function testEndRejectsOnErrored() {
+  const filePath = path.join(tmpDir, 'writer-end-errored.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await w.write(Buffer.from('data'));
+  w.fail(new Error('test error'));
+
+  await assert.rejects(
+    w.end(),
+    { message: 'test error' },
+  );
+  await fh.close();
+}
+
+// =============================================================================
+// end() is idempotent when closing/closed
+// =============================================================================
+
+async function testEndIdempotent() {
+  const filePath = path.join(tmpDir, 'writer-end-idempotent.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await w.write(Buffer.from('data'));
+
+  // Call end() twice concurrently - second should return same promise
+  const p1 = w.end();
+  const p2 = w.end();
+  const [bytes1, bytes2] = await Promise.all([p1, p2]);
+
+  assert.strictEqual(bytes1, 4);
+  assert.strictEqual(bytes2, 4);
+
+  // After closed, calling end() again returns totalBytesWritten
+  const bytes3 = await w.end();
+  assert.strictEqual(bytes3, 4);
+
+  await fh.close();
+}
+
+// =============================================================================
+// asyncDispose waits for pending end() when closing
+// =============================================================================
+
+async function testAsyncDisposeWhileClosing() {
+  const filePath = path.join(tmpDir, 'writer-dispose-closing.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ autoClose: true });
+
+  await w.write(Buffer.from('closing test'));
+
+  // Start end() but don't await - writer is now "closing"
+  const endPromise = w.end();
+
+  // asyncDispose should wait for the pending end, not call fail()
+  await w[Symbol.asyncDispose]();
+  await endPromise;
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'closing test');
+}
+
+// =============================================================================
+// asyncDispose calls fail() on open writer (not graceful cleanup)
+// =============================================================================
+
+async function testAsyncDisposeCallsFail() {
+  const filePath = path.join(tmpDir, 'writer-dispose-fails.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+
+  await w.write(Buffer.from('some data'));
+
+  // Dispose without end() - should call fail(), not graceful cleanup
+  await w[Symbol.asyncDispose]();
+
+  // Writer should be in errored state - write should reject
+  await assert.rejects(
+    w.write(Buffer.from('more')),
+    (err) => err instanceof Error,
+  );
+
+  // Handle should be unlocked and reusable
+  const w2 = fh.writer();
+  await w2.end();
+  await fh.close();
+}
+
+// =============================================================================
+// writer() with limit - async write within limit succeeds
+// =============================================================================
+
+async function testWriterLimit() {
+  const filePath = path.join(tmpDir, 'writer-limit.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ limit: 10 });
+
+  await w.write(Buffer.from('12345'));  // 5 bytes, 5 remaining
+  await w.write(Buffer.from('67890'));  // 5 bytes, 0 remaining
+  const totalBytes = await w.end();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 10);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '1234567890');
+}
+
+// =============================================================================
+// writer() with limit - async write exceeding limit rejects
+// =============================================================================
+
+async function testWriterLimitExceeded() {
+  const filePath = path.join(tmpDir, 'writer-limit-exceeded.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ limit: 5 });
+
+  await w.write(Buffer.from('123'));  // 3 bytes, 2 remaining
+
+  await assert.rejects(
+    w.write(Buffer.from('45678')),  // 5 bytes > 2 remaining
+    { code: 'ERR_OUT_OF_RANGE' },
+  );
+
+  await w.end();
+  await fh.close();
+}
+
+// =============================================================================
+// writer() with limit - writev exceeding limit rejects
+// =============================================================================
+
+async function testWriterLimitWritev() {
+  const filePath = path.join(tmpDir, 'writer-limit-writev.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ limit: 6 });
+
+  await w.writev([Buffer.from('ab'), Buffer.from('cd')]);  // 4 bytes
+
+  await assert.rejects(
+    w.writev([Buffer.from('ef'), Buffer.from('gh')]),  // 4 bytes > 2 remaining
+    { code: 'ERR_OUT_OF_RANGE' },
+  );
+
+  await w.end();
+  await fh.close();
+}
+
+// =============================================================================
+// writer() with limit - writeSync returns false when exceeding limit
+// =============================================================================
+
+async function testWriterLimitWriteSync() {
+  const filePath = path.join(tmpDir, 'writer-limit-writesync.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ limit: 10 });
+
+  assert.strictEqual(w.writeSync(Buffer.from('12345')), true);   // 5 ok
+  assert.strictEqual(w.writeSync(Buffer.from('678')), true);     // 3 ok
+  assert.strictEqual(w.writeSync(Buffer.from('901')), false);    // 3 > 2 remaining
+
+  const totalBytes = w.endSync();
+  await fh.close();
+
+  assert.strictEqual(totalBytes, 8);
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '12345678');
+}
+
+// =============================================================================
+// writer() with limit - writevSync returns false when exceeding limit
+// =============================================================================
+
+async function testWriterLimitWritevSync() {
+  const filePath = path.join(tmpDir, 'writer-limit-writevsync.txt');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer({ limit: 5 });
+
+  assert.strictEqual(w.writevSync([Buffer.from('ab')]), true);
+  // 4 bytes > 3 remaining
+  assert.strictEqual(
+    w.writevSync([Buffer.from('cd'), Buffer.from('ef')]), false);
+
+  w.endSync();
+  await fh.close();
+}
+
+// =============================================================================
+// writer() with limit + start
+// =============================================================================
+
+async function testWriterLimitAndStart() {
+  const filePath = path.join(tmpDir, 'writer-limit-start.txt');
+  // Pre-fill file with dots
+  fs.writeFileSync(filePath, '...........');  // 11 dots
+
+  const fh = await open(filePath, 'r+');
+  const w = fh.writer({ start: 3, limit: 5 });
+
+  await w.write(Buffer.from('HELLO'));  // Write at offset 3
+  await w.end();
+  await fh.close();
+
+  assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '...HELLO...');
+}
+
+// =============================================================================
+// Argument validation
+// =============================================================================
+
+async function testWriterArgumentValidation() {
+  const filePath = path.join(tmpDir, 'pull-arg-validation.txt');
+  fs.writeFileSync(filePath, 'data');
+
+  const fh = await open(filePath, 'r');
+  try {
+    assert.throws(() => fh.writer({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.writer({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.writer({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.writer({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => fh.writer({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.writer({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+    assert.throws(() => fh.writer({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' });
+  } finally {
+    await fh.close();
+  }
+}
+
+// =============================================================================
+// Run all tests
+// =============================================================================
+
+Promise.all([
+  testBasicWrite(),
+  testBasicWritev(),
+  testMixedWriteAndWritev(),
+  testEndReturnsTotalBytes(),
+  testAutoCloseOnEnd(),
+  testAutoCloseOnFail(),
+  testStartOption(),
+  testStartSequentialPosition(),
+  testLockedState(),
+  testUnlockAfterEnd(),
+  testUnlockAfterFail(),
+  testWriteAfterEndRejects(),
+  testClosedHandle(),
+  testPipeToIntegration(),
+  testPipeToWithTransform(),
+  testCompressRoundTrip(),
+  testLargeFileWrite(),
+  testAsyncDispose(),
+  testAsyncDisposeOnError(),
+  testWriteWithAbortedSignalRejects(),
+  testWritevWithAbortedSignalRejects(),
+  testEndWithAbortedSignalRejects(),
+  testWriteString(),
+  testWriteStringMultibyte(),
+  testWritevStrings(),
+  testWritevMixed(),
+  testSyncDispose(),
+  testSyncDisposeOnError(),
+  testWriteSyncBasic(),
+  testWritevSyncBasic(),
+  testWriteSyncLargeChunk(),
+  testWriteSyncReturnsFalseDuringAsync(),
+  testWriteSyncClosedErrored(),
+  testEndSyncBasic(),
+  testEndSyncReturnsFalseDuringAsync(),
+  testEndSyncIdempotent(),
+  testEndSyncAutoClose(),
+  testFullSyncPipeline(),
+  testEndRejectsOnErrored(),
+  testEndIdempotent(),
+  testAsyncDisposeWhileClosing(),
+  testAsyncDisposeCallsFail(),
+  testWriterLimit(),
+  testWriterLimitExceeded(),
+  testWriterLimitWritev(),
+  testWriterLimitWriteSync(),
+  testWriterLimitWritevSync(),
+  testWriterLimitAndStart(),
+  testWriterArgumentValidation(),
+]).then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-mkdtempDisposable.js b/test/js/node/test/parallel/test-fs-promises-mkdtempDisposable.js
new file mode 100644
index 000000000000..8f51a50f7899
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-mkdtempDisposable.js
@@ -0,0 +1,97 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const fsPromises = require('fs/promises');
+const path = require('path');
+const { isMainThread } = require('worker_threads');
+
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh();
+
+async function basicUsage() {
+  const result = await fsPromises.mkdtempDisposable(tmpdir.resolve('foo.'));
+
+  assert.strictEqual(path.basename(result.path).length, 'foo.XXXXXX'.length);
+  assert.strictEqual(path.dirname(result.path), tmpdir.path);
+  assert(fs.existsSync(result.path));
+
+  await result.remove();
+
+  assert(!fs.existsSync(result.path));
+
+  // Second removal does not throw error
+  result.remove();
+}
+
+async function symbolAsyncDispose() {
+  const result = await fsPromises.mkdtempDisposable(tmpdir.resolve('foo.'));
+
+  assert(fs.existsSync(result.path));
+
+  await result[Symbol.asyncDispose]();
+
+  assert(!fs.existsSync(result.path));
+
+  // Second removal does not throw error
+  await result[Symbol.asyncDispose]();
+}
+
+async function chdirDoesNotAffectRemoval() {
+  // Can't use chdir in workers
+  if (!isMainThread) return;
+
+  const originalCwd = process.cwd();
+
+  process.chdir(tmpdir.path);
+  const first = await fsPromises.mkdtempDisposable('first.');
+  const second = await fsPromises.mkdtempDisposable('second.');
+
+  const fullFirstPath = path.join(tmpdir.path, first.path);
+  const fullSecondPath = path.join(tmpdir.path, second.path);
+
+  assert(fs.existsSync(fullFirstPath));
+  assert(fs.existsSync(fullSecondPath));
+
+  process.chdir(fullFirstPath);
+  await second.remove();
+
+  assert(!fs.existsSync(fullSecondPath));
+
+  process.chdir(tmpdir.path);
+  await first.remove();
+  assert(!fs.existsSync(fullFirstPath));
+
+  process.chdir(originalCwd);
+}
+
+async function errorsAreReThrown() {
+  // It is difficult to arrange for rmdir to fail on windows
+  if (common.isWindows || process.getuid() === 0) return;
+  const base = await fsPromises.mkdtempDisposable(tmpdir.resolve('foo.'));
+
+  // On Unix we can prevent removal by making the parent directory read-only
+  const child = await fsPromises.mkdtempDisposable(path.join(base.path, 'bar.'));
+
+  const originalMode = fs.statSync(base.path).mode;
+  fs.chmodSync(base.path, 0o444);
+
+  await assert.rejects(child.remove(), /EACCES|EPERM/);
+
+  fs.chmodSync(base.path, originalMode);
+
+  // Removal works once permissions are reset
+  await child.remove();
+  assert(!fs.existsSync(child.path));
+
+  await base.remove();
+  assert(!fs.existsSync(base.path));
+}
+
+(async () => {
+  await basicUsage();
+  await symbolAsyncDispose();
+  await chdirDoesNotAffectRemoval();
+  await errorsAreReThrown();
+})().then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-readfile-empty.js b/test/js/node/test/parallel/test-fs-promises-readfile-empty.js
index ef15a2681123..f71057a116e6 100644
--- a/test/js/node/test/parallel/test-fs-promises-readfile-empty.js
+++ b/test/js/node/test/parallel/test-fs-promises-readfile-empty.js
@@ -1,5 +1,5 @@
 'use strict';
-require('../common');
+const common = require('../common');
 
 const assert = require('assert');
 const { promises: fs } = require('fs');
@@ -8,10 +8,13 @@ const fixtures = require('../common/fixtures');
 const fn = fixtures.path('empty.txt');
 
 fs.readFile(fn)
-  .then(assert.ok);
+  .then(assert.ok)
+  .then(common.mustCall());
 
 fs.readFile(fn, 'utf8')
-  .then(assert.strictEqual.bind(this, ''));
+  .then(assert.strictEqual.bind(this, ''))
+  .then(common.mustCall());
 
 fs.readFile(fn, { encoding: 'utf8' })
-  .then(assert.strictEqual.bind(this, ''));
+  .then(assert.strictEqual.bind(this, ''))
+  .then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-statfs-validate-path.js b/test/js/node/test/parallel/test-fs-promises-statfs-validate-path.js
new file mode 100644
index 000000000000..64928521f504
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-statfs-validate-path.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const common = require('../common');
+const fs = require('fs');
+const assert = require('assert');
+
+(async () => {
+  await assert.rejects(
+    fs.promises.statfs(),
+    { code: 'ERR_INVALID_ARG_TYPE' },
+  );
+})().then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-ignore-function.mjs b/test/js/node/test/parallel/test-fs-promises-watch-ignore-function.mjs
new file mode 100644
index 000000000000..a2b38250b1fb
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-ignore-function.mjs
@@ -0,0 +1,46 @@
+import * as common from '../common/index.mjs';
+import { skipIfNoWatch } from '../common/watch.js';
+
+skipIfNoWatch();
+
+const assert = await import('node:assert');
+const path = await import('node:path');
+const tmpdir = await import('../common/tmpdir.js');
+const { setTimeout } = await import('node:timers/promises');
+const { watch } = await import('node:fs/promises');
+const { writeFileSync } = await import('node:fs');
+
+tmpdir.refresh();
+
+const testDir = tmpdir.resolve();
+const keepFile = 'visible.txt';
+const ignoreFile = '.hidden';
+const keepFilePath = path.join(testDir, keepFile);
+const ignoreFilePath = path.join(testDir, ignoreFile);
+
+async function watchDir() {
+  const watcher = watch(testDir, {
+    ignore: (filename) => filename.startsWith('.'),
+  });
+
+  for await (const { filename } of watcher) {
+    assert.notStrictEqual(filename, ignoreFile);
+
+    if (filename === keepFile) {
+      break;
+    }
+  }
+}
+
+async function writeFiles() {
+  if (common.isMacOS) {
+    // Do the write with a delay to ensure that the OS is ready to notify us.
+    // See https://github.com/nodejs/node/issues/52601.
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  writeFileSync(ignoreFilePath, 'ignored');
+  writeFileSync(keepFilePath, 'content');
+}
+
+await Promise.all([watchDir(), writeFiles()]);
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-ignore-glob.mjs b/test/js/node/test/parallel/test-fs-promises-watch-ignore-glob.mjs
new file mode 100644
index 000000000000..f8510877d74c
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-ignore-glob.mjs
@@ -0,0 +1,44 @@
+import * as common from '../common/index.mjs';
+import { skipIfNoWatch } from '../common/watch.js';
+
+skipIfNoWatch();
+
+const assert = await import('node:assert');
+const path = await import('node:path');
+const tmpdir = await import('../common/tmpdir.js');
+const { setTimeout } = await import('node:timers/promises');
+const { watch } = await import('node:fs/promises');
+const { writeFileSync } = await import('node:fs');
+
+tmpdir.refresh();
+
+const testDir = tmpdir.resolve();
+const keepFile = 'keep.txt';
+const ignoreFile = 'ignore.log';
+const keepFilePath = path.join(testDir, keepFile);
+const ignoreFilePath = path.join(testDir, ignoreFile);
+
+async function watchDir() {
+  const watcher = watch(testDir, { ignore: '*.log' });
+
+  for await (const { filename } of watcher) {
+    assert.notStrictEqual(filename, ignoreFile);
+
+    if (filename === keepFile) {
+      break;
+    }
+  }
+}
+
+async function writeFiles() {
+  if (common.isMacOS) {
+    // Do the write with a delay to ensure that the OS is ready to notify us.
+    // See https://github.com/nodejs/node/issues/52601.
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  writeFileSync(ignoreFilePath, 'ignored');
+  writeFileSync(keepFilePath, 'content');
+}
+
+await Promise.all([watchDir(), writeFiles()]);
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-ignore-invalid.mjs b/test/js/node/test/parallel/test-fs-promises-watch-ignore-invalid.mjs
new file mode 100644
index 000000000000..597fecc3bb04
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-ignore-invalid.mjs
@@ -0,0 +1,35 @@
+import '../common/index.mjs';
+import { skipIfNoWatch } from '../common/watch.js';
+
+skipIfNoWatch();
+
+const assert = await import('node:assert');
+const { watch } = await import('node:fs/promises');
+
+await assert.rejects(
+  async () => {
+    const watcher = watch('.', { ignore: 123 });
+    // eslint-disable-next-line no-unused-vars
+    for await (const _ of watcher) {
+      // Will throw before yielding
+    }
+  },
+  {
+    code: 'ERR_INVALID_ARG_TYPE',
+    name: 'TypeError',
+  }
+);
+
+await assert.rejects(
+  async () => {
+    const watcher = watch('.', { ignore: '' });
+    // eslint-disable-next-line no-unused-vars
+    for await (const _ of watcher) {
+      // Will throw before yielding
+    }
+  },
+  {
+    code: 'ERR_INVALID_ARG_VALUE',
+    name: 'TypeError',
+  }
+);
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-ignore-mixed.mjs b/test/js/node/test/parallel/test-fs-promises-watch-ignore-mixed.mjs
new file mode 100644
index 000000000000..fd0ced49bd7c
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-ignore-mixed.mjs
@@ -0,0 +1,53 @@
+import * as common from '../common/index.mjs';
+import { skipIfNoWatch } from '../common/watch.js';
+
+skipIfNoWatch();
+
+const assert = await import('node:assert');
+const path = await import('node:path');
+const tmpdir = await import('../common/tmpdir.js');
+const { setTimeout } = await import('node:timers/promises');
+const { watch } = await import('node:fs/promises');
+const { writeFileSync } = await import('node:fs');
+
+tmpdir.refresh();
+
+const testDir = tmpdir.resolve();
+const keepFile = 'keep.txt';
+const ignoreLog = 'debug.log';
+const ignoreTmp = 'temp.tmp';
+const keepFilePath = path.join(testDir, keepFile);
+const ignoreLogPath = path.join(testDir, ignoreLog);
+const ignoreTmpPath = path.join(testDir, ignoreTmp);
+
+async function watchDir() {
+  const watcher = watch(testDir, {
+    ignore: [
+      '*.log',
+      /\.tmp$/,
+    ],
+  });
+
+  for await (const { filename } of watcher) {
+    assert.notStrictEqual(filename, ignoreLog);
+    assert.notStrictEqual(filename, ignoreTmp);
+
+    if (filename === keepFile) {
+      break;
+    }
+  }
+}
+
+async function writeFiles() {
+  if (common.isMacOS) {
+    // Do the write with a delay to ensure that the OS is ready to notify us.
+    // See https://github.com/nodejs/node/issues/52601.
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  writeFileSync(ignoreLogPath, 'ignored');
+  writeFileSync(ignoreTmpPath, 'ignored');
+  writeFileSync(keepFilePath, 'content');
+}
+
+await Promise.all([watchDir(), writeFiles()]);
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-ignore-regexp.mjs b/test/js/node/test/parallel/test-fs-promises-watch-ignore-regexp.mjs
new file mode 100644
index 000000000000..7d8bf7362e51
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-ignore-regexp.mjs
@@ -0,0 +1,44 @@
+import * as common from '../common/index.mjs';
+import { skipIfNoWatch } from '../common/watch.js';
+
+skipIfNoWatch();
+
+const assert = await import('node:assert');
+const path = await import('node:path');
+const tmpdir = await import('../common/tmpdir.js');
+const { setTimeout } = await import('node:timers/promises');
+const { watch } = await import('node:fs/promises');
+const { writeFileSync } = await import('node:fs');
+
+tmpdir.refresh();
+
+const testDir = tmpdir.resolve();
+const keepFile = 'keep.txt';
+const ignoreFile = 'ignore.tmp';
+const keepFilePath = path.join(testDir, keepFile);
+const ignoreFilePath = path.join(testDir, ignoreFile);
+
+async function watchDir() {
+  const watcher = watch(testDir, { ignore: /\.tmp$/ });
+
+  for await (const { filename } of watcher) {
+    assert.notStrictEqual(filename, ignoreFile);
+
+    if (filename === keepFile) {
+      break;
+    }
+  }
+}
+
+async function writeFiles() {
+  if (common.isMacOS) {
+    // Do the write with a delay to ensure that the OS is ready to notify us.
+    // See https://github.com/nodejs/node/issues/52601.
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  writeFileSync(ignoreFilePath, 'ignored');
+  writeFileSync(keepFilePath, 'content');
+}
+
+await Promise.all([watchDir(), writeFiles()]);
diff --git a/test/js/node/test/parallel/test-fs-promises-watch-iterator.js b/test/js/node/test/parallel/test-fs-promises-watch-iterator.js
new file mode 100644
index 000000000000..4cd181abaf70
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-promises-watch-iterator.js
@@ -0,0 +1,66 @@
+'use strict';
+// This tests that when there is a burst of fs watch events, the events
+// emitted after the consumer receives the initial event and before the
+// control returns back to fs.watch() can be queued up and show up
+// in the next iteration.
+const common = require('../common');
+const { watch, writeFile } = require('fs/promises');
+const fs = require('fs');
+const assert = require('assert');
+const { join } = require('path');
+const { setTimeout } = require('timers/promises');
+const { skipIfNoWatch } = require('../common/watch');
+const tmpdir = require('../common/tmpdir');
+
+skipIfNoWatch();
+
+class WatchTestCase {
+  constructor(dirName, files) {
+    this.dirName = dirName;
+    this.files = files;
+  }
+  get dirPath() { return tmpdir.resolve(this.dirName); }
+  filePath(fileName) { return join(this.dirPath, fileName); }
+
+  async run() {
+    await Promise.all([this.watchFiles(), this.writeFiles()]);
+    // eslint-disable-next-line node-core/must-call-assert
+    assert(!this.files.length);
+  }
+  async watchFiles() {
+    const watcher = watch(this.dirPath);
+    for await (const evt of watcher) {
+      const idx = this.files.indexOf(evt.filename);
+      if (idx < 0) continue;
+      this.files.splice(idx, 1);
+      await setTimeout(common.platformTimeout(100));
+      if (!this.files.length) break;
+    }
+  }
+  async writeFiles() {
+    if (common.isMacOS) {
+      // Do the write with a delay to ensure that the OS is ready to notify us.
+      // See https://github.com/nodejs/node/issues/52601.
+      await setTimeout(common.platformTimeout(100));
+    }
+
+    for (const fileName of [...this.files]) {
+      await writeFile(this.filePath(fileName), Date.now() + fileName.repeat(1e4));
+    }
+  }
+}
+
+const kCases = [
+  // Watch on a directory should callback with a filename on supported systems
+  new WatchTestCase(
+    'watch1',
+    ['foo', 'bar', 'baz']
+  ),
+];
+
+tmpdir.refresh();
+
+for (const testCase of kCases) {
+  fs.mkdirSync(testCase.dirPath);
+  testCase.run().then(common.mustCall());
+}
diff --git a/test/js/node/test/parallel/test-fs-promises-writefile.js b/test/js/node/test/parallel/test-fs-promises-writefile.js
index 71805b9552c4..25df61b2b484 100644
--- a/test/js/node/test/parallel/test-fs-promises-writefile.js
+++ b/test/js/node/test/parallel/test-fs-promises-writefile.js
@@ -162,32 +162,18 @@ async function doReadWithEncoding() {
 }
 
 (async () => {
-  console.log("doWrite");
   await doWrite();
-  console.log("doWriteWithCancel");
   await doWriteWithCancel();
-  console.log("doAppend");
   await doAppend();
-  console.log("doRead");
   await doRead();
-  console.log("doReadWithEncoding");
   await doReadWithEncoding();
-  console.log("doWriteStream");
   await doWriteStream();
-  console.log("doWriteStreamWithCancel");
   await doWriteStreamWithCancel();
-  console.log("doWriteIterable");
   await doWriteIterable();
-  console.log("doWriteInvalidIterable");
   await doWriteInvalidIterable();
-  console.log("doWriteIterableWithEncoding");
   await doWriteIterableWithEncoding();
-  console.log("doWriteBufferIterable");
   await doWriteBufferIterable();
-  console.log("doWriteAsyncIterable");
   await doWriteAsyncIterable();
-  console.log("doWriteAsyncLargeIterable");
   await doWriteAsyncLargeIterable();
-  console.log("doWriteInvalidValues");
   await doWriteInvalidValues();
 })().then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-read-offset-null.js b/test/js/node/test/parallel/test-fs-read-offset-null.js
index 012c94e41e92..4104fe141131 100644
--- a/test/js/node/test/parallel/test-fs-read-offset-null.js
+++ b/test/js/node/test/parallel/test-fs-read-offset-null.js
@@ -35,30 +35,25 @@ fs.open(filepath, 'r', common.mustSucceed((fd) => {
           }));
 }));
 
-let filehandle = null;
-
 // Tests for promises api
 (async () => {
-  filehandle = await fsPromises.open(filepath, 'r');
+  await using filehandle = await fsPromises.open(filepath, 'r');
   const readObject = await filehandle.read(buf, { offset: null });
   assert.strictEqual(readObject.buffer[0], 120);
 })()
-.finally(() => filehandle?.close())
 .then(common.mustCall());
 
 // Undocumented: omitted position works the same as position === null
 (async () => {
-  filehandle = await fsPromises.open(filepath, 'r');
+  await using filehandle = await fsPromises.open(filepath, 'r');
   const readObject = await filehandle.read(buf, null, buf.length);
   assert.strictEqual(readObject.buffer[0], 120);
 })()
-.finally(() => filehandle?.close())
 .then(common.mustCall());
 
 (async () => {
-  filehandle = await fsPromises.open(filepath, 'r');
+  await using filehandle = await fsPromises.open(filepath, 'r');
   const readObject = await filehandle.read(buf, null, buf.length, 0);
   assert.strictEqual(readObject.buffer[0], 120);
 })()
-.finally(() => filehandle?.close())
 .then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-read-stream-encoding.js b/test/js/node/test/parallel/test-fs-read-stream-encoding.js
index 8eeaee6572bf..dec5807cc45b 100644
--- a/test/js/node/test/parallel/test-fs-read-stream-encoding.js
+++ b/test/js/node/test/parallel/test-fs-read-stream-encoding.js
@@ -1,5 +1,5 @@
 'use strict';
-require('../common');
+const common = require('../common');
 const assert = require('assert');
 const fs = require('fs');
 const stream = require('stream');
@@ -8,10 +8,10 @@ const encoding = 'base64';
 
 const example = fixtures.path('x.txt');
 const assertStream = new stream.Writable({
-  write: function(chunk, enc, next) {
+  write: common.mustCall((chunk, enc, next) => {
     const expected = Buffer.from('xyz');
     assert(chunk.equals(expected));
-  }
+  }),
 });
 assertStream.setDefaultEncoding(encoding);
 fs.createReadStream(example, encoding).pipe(assertStream);
diff --git a/test/js/node/test/parallel/test-fs-read-stream-err.js b/test/js/node/test/parallel/test-fs-read-stream-err.js
index 1d280f64874f..48abe6f88192 100644
--- a/test/js/node/test/parallel/test-fs-read-stream-err.js
+++ b/test/js/node/test/parallel/test-fs-read-stream-err.js
@@ -42,7 +42,7 @@ fs.close = common.mustCall((fd_, cb) => {
 });
 
 const read = fs.read;
-fs.read = function() {
+fs.read = common.mustCall(function() {
   // First time is ok.
   read.apply(fs, arguments);
   // Then it breaks.
@@ -56,7 +56,7 @@ fs.read = function() {
       throw new Error('BOOM!');
     };
   });
-};
+});
 
 stream.on('data', (buf) => {
   stream.on('data', common.mustNotCall("no more 'data' events should follow"));
diff --git a/test/js/node/test/parallel/test-fs-read-stream-inherit.js b/test/js/node/test/parallel/test-fs-read-stream-inherit.js
index ec090465d4d9..4345ec2df772 100644
--- a/test/js/node/test/parallel/test-fs-read-stream-inherit.js
+++ b/test/js/node/test/parallel/test-fs-read-stream-inherit.js
@@ -52,7 +52,7 @@ const rangeFile = fixtures.path('x.txt');
 {
   const file = fs.createReadStream(fn, { __proto__: { encoding: 'utf8' } });
   file.length = 0;
-  file.on('data', function(data) {
+  file.on('data', common.mustCallAtLeast((data) => {
     assert.strictEqual(typeof data, 'string');
     file.length += data.length;
 
@@ -60,7 +60,7 @@ const rangeFile = fixtures.path('x.txt');
       // http://www.fileformat.info/info/unicode/char/2026/index.htm
       assert.strictEqual(data[i], '\u2026');
     }
-  });
+  }));
 
   file.on('close', common.mustCall(function() {
     assert.strictEqual(file.length, 10000);
diff --git a/test/js/node/test/parallel/test-fs-read-stream-pos.js b/test/js/node/test/parallel/test-fs-read-stream-pos.js
new file mode 100644
index 000000000000..8a5812f81ef2
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-read-stream-pos.js
@@ -0,0 +1,82 @@
+'use strict';
+
+// Refs: https://github.com/nodejs/node/issues/33940
+
+const common = require('../common');
+const tmpdir = require('../common/tmpdir');
+const fs = require('fs');
+const assert = require('assert');
+
+tmpdir.refresh();
+
+const file = tmpdir.resolve('read_stream_pos_test.txt');
+
+fs.writeFileSync(file, '');
+
+let counter = 0;
+
+const writeInterval = setInterval(() => {
+  counter = counter + 1;
+  const line = `hello at ${counter}\n`;
+  fs.writeFileSync(file, line, { flag: 'a' });
+}, 1);
+
+const hwm = 10;
+let bufs = [];
+let isLow = false;
+let cur = 0;
+let stream;
+
+const readInterval = setInterval(common.mustCallAtLeast(() => {
+  if (stream) return;
+
+  stream = fs.createReadStream(file, {
+    highWaterMark: hwm,
+    start: cur
+  });
+  stream.on('data', common.mustCallAtLeast((chunk) => {
+    cur += chunk.length;
+    bufs.push(chunk);
+    if (isLow) {
+      const brokenLines = Buffer.concat(bufs).toString()
+        .split('\n')
+        .filter((line) => {
+          const s = 'hello at'.slice(0, line.length);
+          if (line && !line.startsWith(s)) {
+            return true;
+          }
+          return false;
+        });
+      assert.strictEqual(brokenLines.length, 0);
+      exitTest();
+      return;
+    }
+    if (chunk.length !== hwm) {
+      isLow = true;
+    }
+  }));
+  stream.on('end', () => {
+    stream = null;
+    isLow = false;
+    bufs = [];
+  });
+}), 10);
+
+// Time longer than 90 seconds to exit safely
+const endTimer = setTimeout(() => {
+  exitTest();
+}, 90000);
+
+const exitTest = () => {
+  clearInterval(readInterval);
+  clearInterval(writeInterval);
+  clearTimeout(endTimer);
+  if (stream && !stream.destroyed) {
+    stream.on('close', () => {
+      process.exit();
+    });
+    stream.destroy();
+  } else {
+    process.exit();
+  }
+};
diff --git a/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js b/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js
index a01d23d5abdd..7bb6d2976d98 100644
--- a/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js
+++ b/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js
@@ -1,5 +1,5 @@
 'use strict';
-require('../common');
+const common = require('../common');
 const fixtures = require('../common/fixtures');
 const assert = require('assert');
 const fs = require('fs');
@@ -14,11 +14,11 @@ fs.createReadStream(example, null);
 fs.createReadStream(example, 'utf8');
 fs.createReadStream(example, { encoding: 'utf8' });
 
-const createReadStreamErr = (path, opt, error) => {
+const createReadStreamErr = common.mustCallAtLeast((path, opt, error) => {
   assert.throws(() => {
     fs.createReadStream(path, opt);
   }, error);
-};
+});
 
 const typeError = {
   code: 'ERR_INVALID_ARG_TYPE',
diff --git a/test/js/node/test/parallel/test-fs-read-stream.js b/test/js/node/test/parallel/test-fs-read-stream.js
index 80bd7b01c860..8d4b58d47722 100644
--- a/test/js/node/test/parallel/test-fs-read-stream.js
+++ b/test/js/node/test/parallel/test-fs-read-stream.js
@@ -53,7 +53,7 @@ function test1(options) {
     file.resume();
   }));
 
-  file.on('data', function(data) {
+  file.on('data', common.mustCallAtLeast((data) => {
     assert.ok(data instanceof Buffer);
     assert.ok(data.byteOffset % 8 === 0);
     assert.ok(!paused);
@@ -69,7 +69,7 @@ function test1(options) {
       paused = false;
       file.resume();
     }, 10);
-  });
+  }));
 
 
   file.on('end', common.mustCall(function(chunk) {
@@ -100,7 +100,7 @@ test1({
 {
   const file = fs.createReadStream(fn, common.mustNotMutateObjectDeep({ encoding: 'utf8' }));
   file.length = 0;
-  file.on('data', function(data) {
+  file.on('data', common.mustCallAtLeast((data) => {
     assert.strictEqual(typeof data, 'string');
     file.length += data.length;
 
@@ -108,7 +108,7 @@ test1({
       // http://www.fileformat.info/info/unicode/char/2026/index.htm
       assert.strictEqual(data[i], '\u2026');
     }
-  });
+  }));
 
   file.on('close', common.mustCall());
 
diff --git a/test/js/node/test/parallel/test-fs-read-zero-length.js b/test/js/node/test/parallel/test-fs-read-zero-length.js
index ac2efc73f510..1bac7ed21031 100644
--- a/test/js/node/test/parallel/test-fs-read-zero-length.js
+++ b/test/js/node/test/parallel/test-fs-read-zero-length.js
@@ -8,7 +8,7 @@ const fd = fs.openSync(filepath, 'r');
 const bufferAsync = Buffer.alloc(0);
 const bufferSync = Buffer.alloc(0);
 
-fs.read(fd, bufferAsync, 0, 0, 0, common.mustCall((err, bytesRead) => {
+fs.read(fd, bufferAsync, 0, 0, 0, common.mustSucceed((bytesRead) => {
   assert.strictEqual(bytesRead, 0);
   assert.deepStrictEqual(bufferAsync, Buffer.alloc(0));
 }));
diff --git a/test/js/node/test/parallel/test-fs-readdir-recursive.js b/test/js/node/test/parallel/test-fs-readdir-recursive.js
index ffe4d03d0a9d..7cfc0903faa0 100644
--- a/test/js/node/test/parallel/test-fs-readdir-recursive.js
+++ b/test/js/node/test/parallel/test-fs-readdir-recursive.js
@@ -1,15 +1,18 @@
 'use strict';
-const common = require('../common');
-if (common.isWindows) return; // TODO: BUN
-const fs = require('fs');
-const net = require('net');
 
+const { PIPE, mustCall } = require('../common');
 const tmpdir = require('../common/tmpdir');
-tmpdir.refresh();
+const { test } = require('node:test');
+const fs = require('node:fs');
+const net = require('node:net');
 
-const server = net.createServer().listen(common.PIPE, common.mustCall(() => {
-  // The process should not crash
-  // See https://github.com/nodejs/node/issues/52159
-  fs.readdirSync(tmpdir.path, { recursive: true });
-  server.close();
-}));
+test('readdir should not recurse into Unix domain sockets', (t, done) => {
+  tmpdir.refresh();
+  const server = net.createServer().listen(PIPE, mustCall(() => {
+    // The process should not crash
+    // See https://github.com/nodejs/node/issues/52159
+    fs.readdirSync(tmpdir.path, { recursive: true });
+    server.close();
+    done();
+  }));
+});
diff --git a/test/js/node/test/parallel/test-fs-readfile-eof.js b/test/js/node/test/parallel/test-fs-readfile-eof.js
index d7f9e21c5bf1..5020db019497 100644
--- a/test/js/node/test/parallel/test-fs-readfile-eof.js
+++ b/test/js/node/test/parallel/test-fs-readfile-eof.js
@@ -11,12 +11,12 @@ const childType = ['child-encoding', 'child-non-encoding'];
 if (process.argv[2] === childType[0]) {
   fs.readFile('/dev/stdin', 'utf8').then((data) => {
     process.stdout.write(data);
-  });
+  }).then(common.mustCall());
   return;
 } else if (process.argv[2] === childType[1]) {
   fs.readFile('/dev/stdin').then((data) => {
     process.stdout.write(data);
-  });
+  }).then(common.mustCall());
   return;
 }
 
diff --git a/test/js/node/test/parallel/test-fs-readfile-fd.js b/test/js/node/test/parallel/test-fs-readfile-fd.js
index 1779d9f97dbd..c76671951e3e 100644
--- a/test/js/node/test/parallel/test-fs-readfile-fd.js
+++ b/test/js/node/test/parallel/test-fs-readfile-fd.js
@@ -10,37 +10,34 @@ const fn = fixtures.path('empty.txt');
 const tmpdir = require('../common/tmpdir');
 tmpdir.refresh();
 
-tempFd(function(fd, close) {
-  fs.readFile(fd, function(err, data) {
+tempFd(common.mustCall((fd, close) => {
+  fs.readFile(fd, common.mustSucceed((data) => {
     assert.ok(data);
     close();
-  });
-});
+  }));
+}));
 
-tempFd(function(fd, close) {
-  fs.readFile(fd, 'utf8', function(err, data) {
+tempFd(common.mustCall((fd, close) => {
+  fs.readFile(fd, 'utf8', common.mustSucceed((data) => {
     assert.strictEqual(data, '');
     close();
-  });
-});
+  }));
+}));
 
-tempFdSync(function(fd) {
+tempFdSync(common.mustCall((fd) => {
   assert.ok(fs.readFileSync(fd));
-});
+}));
 
-tempFdSync(function(fd) {
+tempFdSync(common.mustCall((fd) => {
   assert.strictEqual(fs.readFileSync(fd, 'utf8'), '');
-});
+}));
 
 function tempFd(callback) {
-  fs.open(fn, 'r', function(err, fd) {
-    assert.ifError(err);
-    callback(fd, function() {
-      fs.close(fd, function(err) {
-        assert.ifError(err);
-      });
-    });
-  });
+  fs.open(fn, 'r', common.mustSucceed((fd) => {
+    callback(fd, common.mustCall(() => {
+      fs.close(fd, common.mustSucceed());
+    }));
+  }));
 }
 
 function tempFdSync(callback) {
diff --git a/test/js/node/test/parallel/test-fs-readfile-pipe-large.js b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js
index fa5fea3ca388..4e035550f438 100644
--- a/test/js/node/test/parallel/test-fs-readfile-pipe-large.js
+++ b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js
@@ -10,10 +10,9 @@ const assert = require('assert');
 const fs = require('fs');
 
 if (process.argv[2] === 'child') {
-  fs.readFile('/dev/stdin', function(er, data) {
-    assert.ifError(er);
+  fs.readFile('/dev/stdin', common.mustSucceed((data) => {
     process.stdout.write(data);
-  });
+  }));
   return;
 }
 
diff --git a/test/js/node/test/parallel/test-fs-readfile-utf8-fast-path.js b/test/js/node/test/parallel/test-fs-readfile-utf8-fast-path.js
new file mode 100644
index 000000000000..18d0d884dfa4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-readfile-utf8-fast-path.js
@@ -0,0 +1,103 @@
+'use strict';
+
+require('../common');
+const fs = require('node:fs');
+const path = require('node:path');
+const assert = require('node:assert');
+const { describe, it } = require('node:test');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+function writeFile(name, buf) {
+  const p = path.join(tmpdir.path, name);
+  fs.writeFileSync(p, buf);
+  return p;
+}
+
+function expectMatches(filePath, rawBuf) {
+  assert.strictEqual(
+    fs.readFileSync(filePath, 'utf8'),
+    rawBuf.toString('utf8'),
+  );
+}
+
+describe('fs.readFileSync utf8 simdutf dispatch', () => {
+  it('empty file', () => {
+    const p = writeFile('empty.txt', Buffer.alloc(0));
+    assert.strictEqual(fs.readFileSync(p, 'utf8'), '');
+  });
+
+  it('ascii small', () => {
+    const buf = Buffer.from('hello');
+    expectMatches(writeFile('tiny-ascii.txt', buf), buf);
+  });
+
+  it('ascii 20KB', () => {
+    const buf = Buffer.alloc(20 * 1024, 0x41);
+    expectMatches(writeFile('medium-ascii.txt', buf), buf);
+  });
+
+  it('ascii 1MB', () => {
+    const buf = Buffer.alloc(1024 * 1024, 0x61);
+    expectMatches(writeFile('large-ascii.txt', buf), buf);
+  });
+
+  it('fd input', () => {
+    const buf = Buffer.alloc(50 * 1024, 0x62);
+    const p = writeFile('fd-ascii.txt', buf);
+    const fd = fs.openSync(p, 'r');
+    try {
+      assert.strictEqual(fs.readFileSync(fd, 'utf8'), buf.toString('utf8'));
+    } finally {
+      fs.closeSync(fd);
+    }
+  });
+
+  it('multibyte UTF-8', () => {
+    const buf = Buffer.from('中文测试 — café — 🚀'.repeat(500), 'utf8');
+    expectMatches(writeFile('multibyte.txt', buf), buf);
+  });
+
+  it('latin1-fits utf8', () => {
+    const buf = Buffer.from('naïve café résumé — niño Köln '.repeat(500), 'utf8');
+    expectMatches(writeFile('latin1-fits.txt', buf), buf);
+  });
+
+  it('invalid: lone continuation byte', () => {
+    const buf = Buffer.from([0x68, 0x69, 0x80, 0x21]);
+    expectMatches(writeFile('invalid-cont.txt', buf), buf);
+  });
+
+  it('invalid: overlong', () => {
+    const buf = Buffer.from([0x41, 0xC0, 0xAF, 0x42]);
+    expectMatches(writeFile('invalid-overlong.txt', buf), buf);
+  });
+
+  it('invalid: surrogate', () => {
+    const buf = Buffer.from([0x41, 0xED, 0xA0, 0x80, 0x42]);
+    expectMatches(writeFile('invalid-surrogate.txt', buf), buf);
+  });
+
+  it('latin1 boundary U+00FF', () => {
+    const buf = Buffer.from('ÿ'.repeat(2048), 'utf8');
+    expectMatches(writeFile('latin1-boundary.txt', buf), buf);
+  });
+
+  it('above latin1 U+0100', () => {
+    const buf = Buffer.from('ĀāĂ'.repeat(1024), 'utf8');
+    expectMatches(writeFile('above-latin1.txt', buf), buf);
+  });
+
+  it('single codepoint each UTF-8 length', () => {
+    for (const cp of [0x41, 0x00E9, 0x4E2D, 0x1F600]) {
+      const buf = Buffer.from(String.fromCodePoint(cp), 'utf8');
+      expectMatches(writeFile(`single-cp-${cp.toString(16)}.txt`, buf), buf);
+    }
+  });
+
+  it('truncated multibyte at EOF', () => {
+    const buf = Buffer.from([0x41, 0xE4, 0xB8]);
+    expectMatches(writeFile('truncated-multibyte.txt', buf), buf);
+  });
+});
diff --git a/test/js/node/test/parallel/test-fs-realpath.js b/test/js/node/test/parallel/test-fs-realpath.js
index 69237e3974e5..6ad074397eb3 100644
--- a/test/js/node/test/parallel/test-fs-realpath.js
+++ b/test/js/node/test/parallel/test-fs-realpath.js
@@ -46,6 +46,7 @@ if (common.isWindows) {
   // Something like "C:\\"
   root = process.cwd().slice(0, 3);
   assertEqualPath = function(path_left, path_right, message) {
+    // eslint-disable-next-line node-core/must-call-assert
     assert
       .strictEqual(path_left.toLowerCase(), path_right.toLowerCase(), message);
   };
@@ -429,15 +430,13 @@ function test_up_multiple(realpath, realpathSync, cb) {
   assertEqualPath(realpathSync(abedabeda), abedabeda_real);
   assertEqualPath(realpathSync(abedabed), abedabed_real);
 
-  realpath(abedabeda, function(er, real) {
-    assert.ifError(er);
+  realpath(abedabeda, common.mustSucceed((real) => {
     assertEqualPath(abedabeda_real, real);
-    realpath(abedabed, function(er, real) {
-      assert.ifError(er);
+    realpath(abedabed, common.mustSucceed((real) => {
       assertEqualPath(abedabed_real, real);
       cb();
-    });
-  });
+    }));
+  }));
 }
 
 
@@ -472,15 +471,13 @@ function test_up_multiple_with_null_options(realpath, realpathSync, cb) {
   assertEqualPath(realpathSync(abedabeda), abedabeda_real);
   assertEqualPath(realpathSync(abedabed), abedabed_real);
 
-  realpath(abedabeda, null, function(er, real) {
-    assert.ifError(er);
+  realpath(abedabeda, null, common.mustSucceed((real) => {
     assertEqualPath(abedabeda_real, real);
-    realpath(abedabed, null, function(er, real) {
-      assert.ifError(er);
+    realpath(abedabed, null, common.mustSucceed((real) => {
       assertEqualPath(abedabed_real, real);
       cb();
-    });
-  });
+    }));
+  }));
 }
 
 // Absolute symlinks with children.
@@ -548,19 +545,17 @@ function test_abs_with_kids(realpath, realpathSync, cb) {
 
 function test_root(realpath, realpathSync, cb) {
   assertEqualPath(root, realpathSync('/'));
-  realpath('/', function(err, result) {
-    assert.ifError(err);
+  realpath('/', common.mustSucceed((result) => {
     assertEqualPath(root, result);
     cb();
-  });
+  }));
 }
 
 function test_root_with_null_options(realpath, realpathSync, cb) {
-  realpath('/', null, function(err, result) {
-    assert.ifError(err);
+  realpath('/', null, common.mustSucceed((result) => {
     assertEqualPath(root, result);
     cb();
-  });
+  }));
 }
 
 // ----------------------------------------------------------------------------
diff --git a/test/js/node/test/parallel/test-fs-rmSync-special-char.js b/test/js/node/test/parallel/test-fs-rmSync-special-char.js
new file mode 100755
index 000000000000..00d7062d8b10
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-rmSync-special-char.js
@@ -0,0 +1,32 @@
+'use strict';
+require('../common');
+const tmpdir = require('../common/tmpdir');
+const assert = require('node:assert');
+const fs = require('node:fs');
+const path = require('node:path');
+
+// This test ensures that fs.rmSync handles non-ASCII characters in file paths,
+// and that errors contain correctly encoded paths and err.path values.
+
+tmpdir.refresh(); // Prepare a clean temporary directory
+
+// Define paths with non-ASCII characters
+const dirPath = path.join(tmpdir.path, '速_dir');
+const filePath = path.join(tmpdir.path, '速.txt');
+
+// Create a directory and a file with non-ASCII characters
+fs.mkdirSync(dirPath);
+fs.writeFileSync(filePath, 'This is a test file with special characters.');
+fs.rmSync(filePath);
+assert.strictEqual(fs.existsSync(filePath), false);
+
+// Ensure rmSync throws an error when trying to remove a directory without recursive
+assert.throws(() => {
+  fs.rmSync(dirPath, { recursive: false });
+}, (err) => {
+  // Assert the error code and check that the error message includes the correct non-ASCII path
+  assert.strictEqual(err.code, 'ERR_FS_EISDIR');
+  assert(err.message.includes(dirPath), 'Error message should include the directory path');
+  assert.strictEqual(err.path, dirPath);
+  return true;
+});
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-error.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-error.js
new file mode 100644
index 000000000000..dbfbdad40aa9
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-error.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const {
+  rmdir,
+  rmdirSync,
+  promises: { rmdir: rmdirPromise }
+} = require('fs');
+
+assert.throws(() => {
+  rmdir('nonexistent', {
+    recursive: true,
+  }, common.mustNotCall());
+}, {
+  code: 'ERR_INVALID_ARG_VALUE',
+});
+
+assert.throws(() => {
+  rmdirSync('nonexistent', {
+    recursive: true,
+  });
+}, {
+  code: 'ERR_INVALID_ARG_VALUE',
+});
+
+assert.rejects(
+  rmdirPromise('nonexistent', { recursive: true }),
+  { code: 'ERR_INVALID_ARG_VALUE' },
+).then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js
deleted file mode 100644
index 69f8a2c53943..000000000000
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js
+++ /dev/null
@@ -1,22 +0,0 @@
-'use strict';
-const common = require('../common');
-const tmpdir = require('../common/tmpdir');
-const assert = require('assert');
-const fs = require('fs');
-
-tmpdir.refresh();
-
-{
-  // Should warn when trying to delete a nonexistent path
-  // common.expectWarning(
-  //   'DeprecationWarning',
-  //   'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
-  //     'will be removed. Use fs.rm(path, { recursive: true }) instead',
-  //   'DEP0147'
-  // );
-  assert.throws(
-    () => fs.rmdirSync(tmpdir.resolve('noexist.txt'),
-                       { recursive: true }),
-    { code: 'ENOENT' }
-  );
-}
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js
deleted file mode 100644
index 6f32959e21dd..000000000000
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js
+++ /dev/null
@@ -1,22 +0,0 @@
-'use strict';
-const common = require('../common');
-const tmpdir = require('../common/tmpdir');
-const assert = require('assert');
-const fs = require('fs');
-
-tmpdir.refresh();
-
-{
-  // common.expectWarning(
-  //   'DeprecationWarning',
-  //   'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
-  //     'will be removed. Use fs.rm(path, { recursive: true }) instead',
-  //   'DEP0147'
-  // );
-  const filePath = tmpdir.resolve('rmdir-recursive.txt');
-  fs.writeFileSync(filePath, '');
-  assert.throws(
-    () => fs.rmdirSync(filePath, { recursive: true }),
-    { code: common.isWindows ? 'ENOENT' : 'ENOTDIR' }
-  );
-}
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js
deleted file mode 100644
index 840310080f68..000000000000
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js
+++ /dev/null
@@ -1,21 +0,0 @@
-'use strict';
-const common = require('../common');
-const tmpdir = require('../common/tmpdir');
-const fs = require('fs');
-
-tmpdir.refresh();
-
-{
-  // Should warn when trying to delete a nonexistent path
-  // common.expectWarning(
-  //   'DeprecationWarning',
-  //   'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
-  //     'will be removed. Use fs.rm(path, { recursive: true }) instead',
-  //   'DEP0147'
-  // );
-  fs.rmdir(
-    tmpdir.resolve('noexist.txt'),
-    { recursive: true },
-    common.mustCall()
-  );
-}
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js
deleted file mode 100644
index f3a503dbd4c1..000000000000
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js
+++ /dev/null
@@ -1,21 +0,0 @@
-'use strict';
-const common = require('../common');
-const tmpdir = require('../common/tmpdir');
-const assert = require('assert');
-const fs = require('fs');
-
-tmpdir.refresh();
-
-{
-  // common.expectWarning(
-  //   'DeprecationWarning',
-  //   'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
-  //     'will be removed. Use fs.rm(path, { recursive: true }) instead',
-  //   'DEP0147'
-  // );
-  const filePath = tmpdir.resolve('rmdir-recursive.txt');
-  fs.writeFileSync(filePath, '');
-  fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => {
-    assert.strictEqual(err.code, common.isWindows ? 'ENOENT' : 'ENOTDIR');
-  }));
-}
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive.js b/test/js/node/test/parallel/test-fs-rmdir-recursive.js
deleted file mode 100644
index d98431682796..000000000000
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive.js
+++ /dev/null
@@ -1,234 +0,0 @@
-// Flags: --expose-internals
-'use strict';
-const common = require('../common');
-const tmpdir = require('../common/tmpdir');
-const assert = require('assert');
-const fs = require('fs');
-const path = require('path');
-// const { validateRmdirOptions } = require('internal/fs/utils');
-
-// common.expectWarning(
-//   'DeprecationWarning',
-//   'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
-//       'will be removed. Use fs.rm(path, { recursive: true }) instead',
-//   'DEP0147'
-// );
-
-// Bun does not have a validateRmdirOptions function
-// Instead, we can just remove a temp file.
-const pathForRmOptions = tmpdir.resolve('pathForRmOptions');
-function validateRmdirOptions(options) {
-  fs.writeFileSync(pathForRmOptions, '');
-  fs.rmSync(pathForRmOptions, options);
-}
-
-tmpdir.refresh();
-
-let count = 0;
-const nextDirPath = (name = 'rmdir-recursive') =>
-  tmpdir.resolve(`${name}-${count++}`);
-
-function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) {
-  fs.mkdirSync(dirname, { recursive: true });
-  fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8');
-
-  const options = { flag: 'wx' };
-
-  for (let f = files; f > 0; f--) {
-    fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options);
-  }
-
-  if (createSymLinks) {
-    // Valid symlink
-    fs.symlinkSync(
-      `f-${depth}-1`,
-      path.join(dirname, `link-${depth}-good`),
-      'file'
-    );
-
-    // Invalid symlink
-    fs.symlinkSync(
-      'does-not-exist',
-      path.join(dirname, `link-${depth}-bad`),
-      'file'
-    );
-  }
-
-  // File with a name that looks like a glob
-  fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options);
-
-  depth--;
-  if (depth <= 0) {
-    return;
-  }
-
-  for (let f = folders; f > 0; f--) {
-    fs.mkdirSync(
-      path.join(dirname, `folder-${depth}-${f}`),
-      { recursive: true }
-    );
-    makeNonEmptyDirectory(
-      depth,
-      files,
-      folders,
-      path.join(dirname, `d-${depth}-${f}`),
-      createSymLinks
-    );
-  }
-}
-
-function removeAsync(dir) {
-  // Removal should fail without the recursive option.
-  fs.rmdir(dir, common.mustCall((err) => {
-    assert.strictEqual(err.syscall, 'rmdir');
-
-    // Removal should fail without the recursive option set to true.
-    fs.rmdir(dir, { recursive: false }, common.mustCall((err) => {
-      assert.strictEqual(err.syscall, 'rmdir');
-
-      // Recursive removal should succeed.
-      fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => {
-        // An error should occur if recursive and the directory does not exist.
-        fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
-          assert.strictEqual(err.code, 'ENOENT');
-          // Attempted removal should fail now because the directory is gone.
-          fs.rmdir(dir, common.mustCall((err) => {
-            assert.strictEqual(err.syscall, 'rmdir');
-          }));
-        }));
-      }));
-    }));
-  }));
-}
-
-// Test the asynchronous version
-{
-  // Create a 4-level folder hierarchy including symlinks
-  let dir = nextDirPath();
-  makeNonEmptyDirectory(4, 10, 2, dir, true);
-  removeAsync(dir);
-
-  // Create a 2-level folder hierarchy without symlinks
-  dir = nextDirPath();
-  makeNonEmptyDirectory(2, 10, 2, dir, false);
-  removeAsync(dir);
-
-  // Create a flat folder including symlinks
-  dir = nextDirPath();
-  makeNonEmptyDirectory(1, 10, 2, dir, true);
-  removeAsync(dir);
-}
-
-// Test the synchronous version.
-{
-  const dir = nextDirPath();
-  makeNonEmptyDirectory(4, 10, 2, dir, true);
-
-  // Removal should fail without the recursive option set to true.
-  assert.throws(() => {
-    fs.rmdirSync(dir);
-  }, { syscall: 'rmdir' });
-  assert.throws(() => {
-    fs.rmdirSync(dir, { recursive: false });
-  }, { syscall: 'rmdir' });
-
-  // Recursive removal should succeed.
-  fs.rmdirSync(dir, { recursive: true });
-
-  // An error should occur if recursive and the directory does not exist.
-  assert.throws(() => fs.rmdirSync(dir, { recursive: true }),
-                { code: 'ENOENT' });
-
-  // Attempted removal should fail now because the directory is gone.
-  assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' });
-}
-
-// Test the Promises based version.
-(async () => {
-  const dir = nextDirPath();
-  makeNonEmptyDirectory(4, 10, 2, dir, true);
-
-  // Removal should fail without the recursive option set to true.
-  await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
-  await assert.rejects(fs.promises.rmdir(dir, { recursive: false }), {
-    syscall: 'rmdir'
-  });
-
-  // Recursive removal should succeed.
-  await fs.promises.rmdir(dir, { recursive: true });
-
-  // An error should occur if recursive and the directory does not exist.
-  await assert.rejects(fs.promises.rmdir(dir, { recursive: true }),
-                       { code: 'ENOENT' });
-
-  // Attempted removal should fail now because the directory is gone.
-  await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
-})().then(common.mustCall());
-
-// Test input validation.
-{
-  const defaults = {
-    retryDelay: 100,
-    maxRetries: 0,
-    recursive: false
-  };
-  const modified = {
-    retryDelay: 953,
-    maxRetries: 5,
-    recursive: true
-  };
-
-  // assert.deepStrictEqual(validateRmdirOptions(), defaults);
-  // assert.deepStrictEqual(validateRmdirOptions({}), defaults);
-  // assert.deepStrictEqual(validateRmdirOptions(modified), modified);
-  // assert.deepStrictEqual(validateRmdirOptions({
-  //   maxRetries: 99
-  // }), {
-  //   retryDelay: 100,
-  //   maxRetries: 99,
-  //   recursive: false
-  // });
-  validateRmdirOptions(defaults);
-  validateRmdirOptions(modified);
-  validateRmdirOptions({
-    maxRetries: 99
-  });
-
-  [null, 'foo', 5, NaN].forEach((bad) => {
-    assert.throws(() => {
-      validateRmdirOptions(bad);
-    }, {
-      code: 'ERR_INVALID_ARG_TYPE',
-      name: 'TypeError',
-      message: /^The "options" argument must be of type object\./
-    });
-  });
-
-  // Bun treats properties that are undefined as unset
-  // [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => {
-  [null, 'foo', Infinity, function() {}].forEach((bad) => {
-    assert.throws(() => {
-      validateRmdirOptions({ recursive: bad });
-    }, {
-      code: 'ERR_INVALID_ARG_TYPE',
-      name: 'TypeError',
-      message: /^The "options\.recursive" property must be of type boolean\./
-    });
-  });
-
-  assert.throws(() => {
-    validateRmdirOptions({ retryDelay: -1 });
-  }, {
-    code: 'ERR_OUT_OF_RANGE',
-    name: 'RangeError',
-    message: /^The value of "options\.retryDelay" is out of range\./
-  });
-
-  assert.throws(() => {
-    validateRmdirOptions({ maxRetries: -1 });
-  }, {
-    code: 'ERR_OUT_OF_RANGE',
-    name: 'RangeError',
-    message: /^The value of "options\.maxRetries" is out of range\./
-  });
-}
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-throws-not-found.js
similarity index 69%
rename from test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js
rename to test/js/node/test/parallel/test-fs-rmdir-throws-not-found.js
index d984fef80e9f..7b8bbbb153aa 100644
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js
+++ b/test/js/node/test/parallel/test-fs-rmdir-throws-not-found.js
@@ -9,7 +9,7 @@ tmpdir.refresh();
 {
   assert.throws(
     () =>
-      fs.rmdirSync(tmpdir.resolve('noexist.txt'), { recursive: true }),
+      fs.rmdirSync(tmpdir.resolve('noexist.txt')),
     {
       code: 'ENOENT',
     }
@@ -18,7 +18,6 @@ tmpdir.refresh();
 {
   fs.rmdir(
     tmpdir.resolve('noexist.txt'),
-    { recursive: true },
     common.mustCall((err) => {
       assert.strictEqual(err.code, 'ENOENT');
     })
@@ -26,8 +25,7 @@ tmpdir.refresh();
 }
 {
   assert.rejects(
-    () => fs.promises.rmdir(tmpdir.resolve('noexist.txt'),
-                            { recursive: true }),
+    () => fs.promises.rmdir(tmpdir.resolve('noexist.txt')),
     {
       code: 'ENOENT',
     }
diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-throws-on-file.js
similarity index 73%
rename from test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js
rename to test/js/node/test/parallel/test-fs-rmdir-throws-on-file.js
index ff67cf536829..d97f623ae518 100644
--- a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js
+++ b/test/js/node/test/parallel/test-fs-rmdir-throws-on-file.js
@@ -11,18 +11,18 @@ const code = common.isWindows ? 'ENOENT' : 'ENOTDIR';
 {
   const filePath = tmpdir.resolve('rmdir-recursive.txt');
   fs.writeFileSync(filePath, '');
-  assert.throws(() => fs.rmdirSync(filePath, { recursive: true }), { code });
+  assert.throws(() => fs.rmdirSync(filePath), { code });
 }
 {
   const filePath = tmpdir.resolve('rmdir-recursive.txt');
   fs.writeFileSync(filePath, '');
-  fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => {
+  fs.rmdir(filePath, common.mustCall((err) => {
     assert.strictEqual(err.code, code);
   }));
 }
 {
   const filePath = tmpdir.resolve('rmdir-recursive.txt');
   fs.writeFileSync(filePath, '');
-  assert.rejects(() => fs.promises.rmdir(filePath, { recursive: true }),
+  assert.rejects(() => fs.promises.rmdir(filePath),
                  { code }).then(common.mustCall());
 }
diff --git a/test/js/node/test/parallel/test-fs-stat-abort-test.js b/test/js/node/test/parallel/test-fs-stat-abort-test.js
new file mode 100644
index 000000000000..2a2b35f8030d
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-stat-abort-test.js
@@ -0,0 +1,34 @@
+'use strict';
+
+require('../common');
+const test = require('node:test');
+const assert = require('node:assert');
+const fs = require('node:fs');
+const tmpdir = require('../common/tmpdir');
+
+test('fs.stat should throw AbortError when called with an already aborted AbortSignal', async () => {
+  // This test verifies that fs.stat immediately throws an AbortError if the provided AbortSignal
+  // has already been canceled. This approach is used because trying to abort an fs.stat call in flight
+  // is unreliable given that file system operations tend to complete very quickly on many platforms.
+  tmpdir.refresh();
+
+  const filePath = tmpdir.resolve('temp.txt');
+  fs.writeFileSync(filePath, 'Test');
+
+  // Create an already aborted AbortSignal.
+  const signal = AbortSignal.abort();
+
+  const { promise, resolve, reject } = Promise.withResolvers();
+  fs.stat(filePath, { signal }, (err, stats) => {
+    if (err) {
+      return reject(err);
+    }
+    resolve(stats);
+  });
+
+  // Assert that the promise is rejected with an AbortError.
+  await assert.rejects(promise, { name: 'AbortError' });
+
+  fs.unlinkSync(filePath);
+  tmpdir.refresh();
+});
diff --git a/test/js/node/test/parallel/test-fs-stat-bigint.js b/test/js/node/test/parallel/test-fs-stat-bigint.js
index 0a2bea92e501..ae8d3857dec8 100644
--- a/test/js/node/test/parallel/test-fs-stat-bigint.js
+++ b/test/js/node/test/parallel/test-fs-stat-bigint.js
@@ -148,7 +148,7 @@ if (!common.isWindows) {
     { code: 'EBADF' });
 }
 
-const runCallbackTest = (func, arg, done) => {
+const runCallbackTest = common.mustCall((func, arg, done) => {
   const startTime = process.hrtime.bigint();
   func(arg, common.mustNotMutateObjectDeep({ bigint: true }), common.mustCall((err, bigintStats) => {
     func(arg, common.mustCall((err, numStats) => {
@@ -160,7 +160,7 @@ const runCallbackTest = (func, arg, done) => {
       }
     }));
   }));
-};
+}, common.isWindows ? 2 : 3);
 
 {
   const filename = getFilename();
diff --git a/test/js/node/test/parallel/test-fs-stat-date.mjs b/test/js/node/test/parallel/test-fs-stat-date.mjs
index 5f85bff2731e..489cd4fc20fd 100644
--- a/test/js/node/test/parallel/test-fs-stat-date.mjs
+++ b/test/js/node/test/parallel/test-fs-stat-date.mjs
@@ -42,6 +42,13 @@ function closeEnough(actual, expected, margin) {
             `expected ${expected} ± ${margin}, got ${actual}`);
 }
 
+// Ensure that accessed atime and mtime are enumerable
+function validateEnumerability(stats) {
+  const keys = Object.keys(stats);
+  assert.ok(keys.includes('atime'));
+  assert.ok(keys.includes('mtime'));
+}
+
 async function runTest(atime, mtime, margin = 0) {
   margin += Number.EPSILON;
   try {
@@ -56,24 +63,28 @@ async function runTest(atime, mtime, margin = 0) {
   closeEnough(stats.mtimeMs, mtime, margin);
   closeEnough(stats.atime.getTime(), new Date(atime).getTime(), margin);
   closeEnough(stats.mtime.getTime(), new Date(mtime).getTime(), margin);
+  validateEnumerability(stats);
 
   const statsBigint = await fsPromises.stat(filepath, { bigint: true });
   closeEnough(statsBigint.atimeMs, BigInt(atime), margin);
   closeEnough(statsBigint.mtimeMs, BigInt(mtime), margin);
   closeEnough(statsBigint.atime.getTime(), new Date(atime).getTime(), margin);
   closeEnough(statsBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
+  validateEnumerability(statsBigint);
 
   const statsSync = fs.statSync(filepath);
   closeEnough(statsSync.atimeMs, atime, margin);
   closeEnough(statsSync.mtimeMs, mtime, margin);
   closeEnough(statsSync.atime.getTime(), new Date(atime).getTime(), margin);
   closeEnough(statsSync.mtime.getTime(), new Date(mtime).getTime(), margin);
+  validateEnumerability(statsSync);
 
   const statsSyncBigint = fs.statSync(filepath, { bigint: true });
   closeEnough(statsSyncBigint.atimeMs, BigInt(atime), margin);
   closeEnough(statsSyncBigint.mtimeMs, BigInt(mtime), margin);
   closeEnough(statsSyncBigint.atime.getTime(), new Date(atime).getTime(), margin);
   closeEnough(statsSyncBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
+  validateEnumerability(statsSyncBigint);
 }
 
 // Too high/low numbers produce too different results on different platforms
diff --git a/test/js/node/test/parallel/test-fs-stat-temporal.mjs b/test/js/node/test/parallel/test-fs-stat-temporal.mjs
new file mode 100644
index 000000000000..67443fc36c94
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-stat-temporal.mjs
@@ -0,0 +1,105 @@
+import * as common from '../common/index.mjs';
+
+// Test `Temporal.Instant`s returned by fsPromises.stat and fs.statSync
+
+import fs from 'node:fs';
+import fsPromises from 'node:fs/promises';
+import assert from 'node:assert';
+import tmpdir from '../common/tmpdir.js';
+
+if (!common.hasTemporal) {
+  common.skip('Temporal support unavailable');
+}
+
+// On some platforms (for example, ppc64) boundaries are tighter
+// than usual. If we catch these errors, skip corresponding test.
+const ignoredErrors = new Set(['EINVAL', 'EOVERFLOW']);
+
+tmpdir.refresh();
+const filepath = tmpdir.resolve('timestamp');
+
+await (await fsPromises.open(filepath, 'w')).close();
+
+// Perform a trivial check to determine if filesystem supports setting
+// and retrieving atime and mtime. If it doesn't, skip the test.
+await fsPromises.utimes(filepath, 2, 2);
+const { atimeMs, mtimeMs } = await fsPromises.stat(filepath);
+if (atimeMs !== 2000 || mtimeMs !== 2000) {
+  common.skip(`Unsupported filesystem (atimeMs=${atimeMs}, mtimeMs=${mtimeMs})`);
+}
+
+// Allow delta due to precision loss or platform-specific nuances
+function closeEnough(actual, expected, margin) {
+  // On ppc64, value is rounded to seconds
+  if (process.arch === 'ppc64') {
+    margin += 1000;
+  }
+
+  // Filesystems without support for timestamps before 1970-01-01, such as NFSv3,
+  // should return 0 for negative numbers. Do not treat it as error.
+  if (actual === 0 && expected < 0) {
+    console.log(`ignored 0 while expecting ${expected}`);
+    return;
+  }
+
+  assert.ok(Math.abs(Number(actual - expected)) < margin,
+            `expected ${expected} ± ${margin}, got ${actual}`);
+}
+
+// Ensure that accessed atime and mtime are enumerable
+function validateEnumerability(stats) {
+  const keys = Object.keys(stats);
+  assert.ok(keys.includes('atimeInstant'));
+  assert.ok(keys.includes('mtimeInstant'));
+}
+
+async function runTest(atimeMs, mtimeMs, margin = 0) {
+  margin += Number.EPSILON;
+
+  // TODO(LiviaMedeiros): use bigint nanoseconds once `utimes()` supports Temporal
+  const atime = atimeMs / 1000;
+  const mtime = mtimeMs / 1000;
+  try {
+    await fsPromises.utimes(filepath, atime, mtime);
+  } catch (e) {
+    if (ignoredErrors.has(e.code)) return;
+    throw e;
+  }
+
+  const stats = await fsPromises.stat(filepath);
+  closeEnough(stats.atimeMs, atimeMs, margin);
+  closeEnough(stats.mtimeMs, mtimeMs, margin);
+  closeEnough(stats.atimeInstant.epochMilliseconds, atimeMs, margin);
+  closeEnough(stats.mtimeInstant.epochMilliseconds, mtimeMs, margin);
+  validateEnumerability(stats);
+
+  const statsBigint = await fsPromises.stat(filepath, { bigint: true });
+  closeEnough(statsBigint.atimeMs, BigInt(atimeMs), margin);
+  closeEnough(statsBigint.mtimeMs, BigInt(mtimeMs), margin);
+  closeEnough(statsBigint.atimeInstant.epochMilliseconds, atimeMs, margin);
+  closeEnough(statsBigint.mtimeInstant.epochMilliseconds, mtimeMs, margin);
+  validateEnumerability(statsBigint);
+
+  const statsSync = fs.statSync(filepath);
+  closeEnough(statsSync.atimeMs, atimeMs, margin);
+  closeEnough(statsSync.mtimeMs, mtimeMs, margin);
+  closeEnough(statsSync.atimeInstant.epochMilliseconds, atimeMs, margin);
+  closeEnough(statsSync.mtimeInstant.epochMilliseconds, mtimeMs, margin);
+  validateEnumerability(statsSync);
+
+  const statsSyncBigint = fs.statSync(filepath, { bigint: true });
+  closeEnough(statsSyncBigint.atimeMs, BigInt(atimeMs), margin);
+  closeEnough(statsSyncBigint.mtimeMs, BigInt(mtimeMs), margin);
+  closeEnough(statsSyncBigint.atimeInstant.epochMilliseconds, atimeMs, margin);
+  closeEnough(statsSyncBigint.mtimeInstant.epochMilliseconds, mtimeMs, margin);
+  validateEnumerability(statsSyncBigint);
+}
+
+// Too high/low numbers produce too different results on different platforms
+{
+  await runTest(0, 0);
+  await runTest(1, 1);
+  await runTest(355, 40691, 1); // Precision loss on 32bit
+  await runTest(40691, 355, 1); // Precision loss on 32bit
+  await runTest(1713037251360, 1713037251360, 1); // Precision loss
+}
diff --git a/test/js/node/test/parallel/test-fs-symlink-dir-junction.js b/test/js/node/test/parallel/test-fs-symlink-dir-junction.js
index 5f46b7f82686..4d5db3b444eb 100644
--- a/test/js/node/test/parallel/test-fs-symlink-dir-junction.js
+++ b/test/js/node/test/parallel/test-fs-symlink-dir-junction.js
@@ -38,9 +38,7 @@ fs.symlink(linkData, linkPath, 'junction', common.mustSucceed(() => {
     assert.ok(stats.isSymbolicLink());
 
     fs.readlink(linkPath, common.mustSucceed((destination) => {
-      // BUN: It was observed that Node.js 22 fails on this line, bun includes the trailing \ too. Make this test looser.
-      const withoutTrailingSlash = str => str.replace(/\\$/, '');
-      assert.strictEqual(withoutTrailingSlash(destination), withoutTrailingSlash(linkData));
+      assert.strictEqual(destination, linkData);
 
       fs.unlink(linkPath, common.mustSucceed(() => {
         assert(!fs.existsSync(linkPath));
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-function.js b/test/js/node/test/parallel/test-fs-watch-ignore-function.js
new file mode 100644
index 000000000000..70cb8caaf656
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-function.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFileName = 'visible.txt';
+const testFilePath = path.join(tmpdir.path, testFileName);
+const ignoredFileName = '.hidden';
+const ignoredFilePath = path.join(tmpdir.path, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  ignore: (filename) => filename.startsWith('.'),
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  assert.notStrictEqual(filename, ignoredFileName);
+
+  if (filename === testFileName) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-glob.js b/test/js/node/test/parallel/test-fs-watch-ignore-glob.js
new file mode 100644
index 000000000000..ad8588adb8d0
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-glob.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFileName = 'file.txt';
+const testFilePath = path.join(tmpdir.path, testFileName);
+const ignoredFileName = 'file.log';
+const ignoredFilePath = path.join(tmpdir.path, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  ignore: '*.log',
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  assert.notStrictEqual(filename, ignoredFileName);
+
+  if (filename === testFileName) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-invalid.js b/test/js/node/test/parallel/test-fs-watch-ignore-invalid.js
new file mode 100644
index 000000000000..e5d4dba0e23f
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-invalid.js
@@ -0,0 +1,41 @@
+'use strict';
+
+require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const fs = require('fs');
+
+assert.throws(
+  () => fs.watch('.', { ignore: 123 }),
+  {
+    code: 'ERR_INVALID_ARG_TYPE',
+    name: 'TypeError',
+  }
+);
+
+assert.throws(
+  () => fs.watch('.', { ignore: '' }),
+  {
+    code: 'ERR_INVALID_ARG_VALUE',
+    name: 'TypeError',
+  }
+);
+
+assert.throws(
+  () => fs.watch('.', { ignore: [123] }),
+  {
+    code: 'ERR_INVALID_ARG_TYPE',
+    name: 'TypeError',
+  }
+);
+
+assert.throws(
+  () => fs.watch('.', { ignore: [''] }),
+  {
+    code: 'ERR_INVALID_ARG_VALUE',
+    name: 'TypeError',
+  }
+);
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-mixed.js b/test/js/node/test/parallel/test-fs-watch-ignore-mixed.js
new file mode 100644
index 000000000000..63752c20676e
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-mixed.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFileName = 'keep.txt';
+const testFilePath = path.join(tmpdir.path, testFileName);
+const ignoredLogName = 'debug.log';
+const ignoredLogPath = path.join(tmpdir.path, ignoredLogName);
+const ignoredTmpName = 'temp.tmp';
+const ignoredTmpPath = path.join(tmpdir.path, ignoredTmpName);
+const ignoredHiddenName = '.secret';
+const ignoredHiddenPath = path.join(tmpdir.path, ignoredHiddenName);
+
+const watcher = fs.watch(tmpdir.path, {
+  ignore: [
+    '*.log',
+    /\.tmp$/,
+    (filename) => filename.startsWith('.'),
+  ],
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  assert.notStrictEqual(filename, ignoredLogName);
+  assert.notStrictEqual(filename, ignoredTmpName);
+  assert.notStrictEqual(filename, ignoredHiddenName);
+
+  if (filename === testFileName) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredLogPath, 'ignored');
+  fs.writeFileSync(ignoredTmpPath, 'ignored');
+  fs.writeFileSync(ignoredHiddenPath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob-subdirectories.js b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob-subdirectories.js
new file mode 100644
index 000000000000..437d71e0c9ef
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob-subdirectories.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const nodeModules = path.join(tmpdir.path, 'node_modules');
+const srcDir = path.join(tmpdir.path, 'src');
+
+fs.mkdirSync(nodeModules);
+fs.mkdirSync(srcDir);
+
+const testFileName = 'app.js';
+const testFilePath = path.join(srcDir, testFileName);
+const ignoredFileName = 'package.json';
+const ignoredFilePath = path.join(nodeModules, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  recursive: true,
+  // On Linux, matching the directory skips watching it entirely.
+  // On macOS, the native watcher still needs to filter file events inside.
+  ignore: ['**/node_modules/**', '**/node_modules'],
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  if (!filename) return;
+
+  // On recursive watch, filename includes relative path from watched dir
+  assert(!filename.includes('node_modules'));
+
+  if (filename.endsWith(testFileName)) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, '{}');
+  fs.writeFileSync(testFilePath, 'console.log("hello-' + Date.now() + '")');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob.js b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob.js
new file mode 100644
index 000000000000..96123d77ee1c
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-glob.js
@@ -0,0 +1,51 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const subDirectory = path.join(tmpdir.path, 'subdir');
+fs.mkdirSync(subDirectory);
+
+const testFileName = 'file.txt';
+const testFilePath = path.join(subDirectory, testFileName);
+const ignoredFileName = 'file.log';
+const ignoredFilePath = path.join(subDirectory, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  recursive: true,
+  ignore: '*.log',
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  if (!filename) return;
+
+  // On recursive watch, filename includes relative path from watched dir
+  assert(!filename.endsWith(ignoredFileName));
+
+  if (filename.endsWith(testFileName)) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-recursive-mixed.js b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-mixed.js
new file mode 100644
index 000000000000..1e88d040e855
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-mixed.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const subDirectory = path.join(tmpdir.path, 'deep');
+fs.mkdirSync(subDirectory);
+
+const testFileName = 'visible.txt';
+const testFilePath = path.join(subDirectory, testFileName);
+const ignoredLogName = 'debug.log';
+const ignoredLogPath = path.join(subDirectory, ignoredLogName);
+const ignoredTmpName = 'temp.tmp';
+const ignoredTmpPath = path.join(subDirectory, ignoredTmpName);
+const ignoredHiddenName = '.gitignore';
+const ignoredHiddenPath = path.join(subDirectory, ignoredHiddenName);
+
+const watcher = fs.watch(tmpdir.path, {
+  recursive: true,
+  ignore: [
+    '*.log',
+    /\.tmp$/,
+    (filename) => path.basename(filename).startsWith('.'),
+  ],
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  if (!filename) return;
+
+  // On recursive watch, filename includes relative path from watched dir
+  assert(!filename.endsWith(ignoredLogName));
+  assert(!filename.endsWith(ignoredTmpName));
+  assert(!filename.endsWith(ignoredHiddenName));
+
+  if (filename.endsWith(testFileName)) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredLogPath, 'ignored');
+  fs.writeFileSync(ignoredTmpPath, 'ignored');
+  fs.writeFileSync(ignoredHiddenPath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-recursive-regexp.js b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-regexp.js
new file mode 100644
index 000000000000..6ae404b72032
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-recursive-regexp.js
@@ -0,0 +1,51 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const subDirectory = path.join(tmpdir.path, 'nested');
+fs.mkdirSync(subDirectory);
+
+const testFileName = 'keep.txt';
+const testFilePath = path.join(subDirectory, testFileName);
+const ignoredFileName = 'temp.tmp';
+const ignoredFilePath = path.join(subDirectory, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  recursive: true,
+  ignore: /\.tmp$/,
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  if (!filename) return;
+
+  // On recursive watch, filename includes relative path from watched dir
+  assert(!filename.endsWith(ignoredFileName));
+
+  if (filename.endsWith(testFileName)) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-ignore-regexp.js b/test/js/node/test/parallel/test-fs-watch-ignore-regexp.js
new file mode 100644
index 000000000000..8a0a6a93dcac
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-ignore-regexp.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const common = require('../common');
+const { skipIfNoWatch } = require('../common/watch.js');
+
+skipIfNoWatch();
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const testFileName = 'keep.txt';
+const testFilePath = path.join(tmpdir.path, testFileName);
+const ignoredFileName = 'ignore.tmp';
+const ignoredFilePath = path.join(tmpdir.path, ignoredFileName);
+
+const watcher = fs.watch(tmpdir.path, {
+  ignore: /\.tmp$/,
+});
+
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  assert.notStrictEqual(filename, ignoredFileName);
+
+  if (filename === testFileName) {
+    watcher.close();
+  }
+}, 1));
+
+function writeFiles() {
+  fs.writeFileSync(ignoredFilePath, 'ignored');
+  fs.writeFileSync(testFilePath, 'content');
+}
+
+if (common.isMacOS) {
+  // Do the write with a delay to ensure that the OS is ready to notify us. See
+  // https://github.com/nodejs/node/issues/52601.
+  setTimeout(writeFiles, common.platformTimeout(100));
+} else {
+  writeFiles();
+}
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js
index 511829fa385e..b5c134c85556 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js
@@ -39,13 +39,13 @@ const relativePath = path.join(file, path.basename(subfolderPath), childrenFile)
 
 const watcher = fs.watch(testDirectory, { recursive: true });
 let watcherClosed = false;
-watcher.on('change', function(event, filename) {
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
   if (filename === relativePath) {
     assert.strictEqual(event, 'rename');
     watcher.close();
     watcherClosed = true;
   }
-});
+}));
 
 // Do the write with a delay to ensure that the OS is ready to notify us.
 setTimeout(() => {
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js
new file mode 100644
index 000000000000..df594f282f1d
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common');
+
+if (common.isIBMi)
+  common.skip('IBMi does not support `fs.watch()`');
+
+// fs-watch on folders have limited capability in AIX.
+// The testcase makes use of folder watching, and causes
+// hang. This behavior is documented. Skip this for AIX.
+
+if (common.isAIX)
+  common.skip('folder watch capability is limited in AIX.');
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+const testDir = tmpdir.path;
+tmpdir.refresh();
+
+// Add a file to newly created folder to already watching folder
+
+const rootDirectory = fs.mkdtempSync(testDir + path.sep);
+const testDirectory = path.join(rootDirectory, 'test-3');
+fs.mkdirSync(testDirectory);
+
+const filePath = path.join(testDirectory, 'folder-3');
+
+const childrenFile = 'file-4.txt';
+const childrenAbsolutePath = path.join(filePath, childrenFile);
+const childrenRelativePath = path.join(path.basename(filePath), childrenFile);
+let watcherClosed = false;
+
+const watcher = fs.watch(testDirectory, { recursive: true });
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
+  if (filename === childrenRelativePath) {
+    assert.strictEqual(event, 'rename');
+    watcher.close();
+    watcherClosed = true;
+  }
+}));
+
+// Do the write with a delay to ensure that the OS is ready to notify us.
+setTimeout(() => {
+  fs.mkdirSync(filePath);
+  fs.writeFileSync(childrenAbsolutePath, 'world');
+}, common.platformTimeout(200));
+
+process.once('exit', function() {
+  assert(watcherClosed, 'watcher Object was not closed');
+});
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js
index 852c7088d597..bb441c173b14 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js
@@ -34,13 +34,13 @@ tmpdir.refresh();
 
   const watcher = fs.watch(url, { recursive: true });
   let watcherClosed = false;
-  watcher.on('change', function(event, filename) {
+  watcher.on('change', common.mustCallAtLeast((event, filename) => {
     if (filename === path.basename(filePath)) {
       assert.strictEqual(event, 'rename');
       watcher.close();
       watcherClosed = true;
     }
-  });
+  }));
 
   await setTimeout(common.platformTimeout(100));
   fs.writeFileSync(filePath, 'world');
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js
index e8724102c89f..d9e5843b936d 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js
@@ -30,13 +30,13 @@ const testFile = path.join(testDirectory, 'file-1.txt');
 
 const watcher = fs.watch(testDirectory, { recursive: true });
 let watcherClosed = false;
-watcher.on('change', function(event, filename) {
+watcher.on('change', common.mustCallAtLeast((event, filename) => {
   if (filename === path.basename(testFile)) {
     assert.strictEqual(event, 'rename');
     watcher.close();
     watcherClosed = true;
   }
-});
+}));
 
 // Do the write with a delay to ensure that the OS is ready to notify us.
 setTimeout(() => {
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js
index 1a6671de2f36..4e508a147cf7 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js
@@ -32,13 +32,13 @@ tmpdir.refresh();
 
   const watcher = fs.watch(testDirectory, { recursive: true });
   let watcherClosed = false;
-  watcher.on('change', function(event, filename) {
+  watcher.on('change', common.mustCallAtLeast((event, filename) => {
     if (filename === path.basename(testFile)) {
       assert.strictEqual(event, 'rename');
       watcher.close();
       watcherClosed = true;
     }
-  });
+  }));
 
   await setTimeout(common.platformTimeout(100));
   fs.mkdirSync(testFile);
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-delete.js b/test/js/node/test/parallel/test-fs-watch-recursive-delete.js
index 8e78ad54d68a..e4ad0f017090 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-delete.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-delete.js
@@ -28,6 +28,6 @@ const onFileUpdate = common.mustCallAtLeast((eventType, filename) => {
 const watcher = fs.watch(toWatch, { recursive: true }, onFileUpdate);
 
 // We must wait a bit `fs.rm()` to let the watcher be set up properly
-setTimeout(() => {
+setTimeout(common.mustCall(() => {
   fs.rm(tmpdir.resolve('./parent/child'), { recursive: true }, common.mustCall());
-}, common.platformTimeout(500));
+}), common.platformTimeout(500));
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-promise.js b/test/js/node/test/parallel/test-fs-watch-recursive-promise.js
new file mode 100644
index 000000000000..cb00a35db274
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-promise.js
@@ -0,0 +1,94 @@
+'use strict';
+
+const common = require('../common');
+
+if (common.isIBMi)
+  common.skip('IBMi does not support `fs.watch()`');
+
+// fs-watch on folders have limited capability in AIX.
+// The testcase makes use of folder watching, and causes
+// hang. This behavior is documented. Skip this for AIX.
+
+if (common.isAIX)
+  common.skip('folder watch capability is limited in AIX.');
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs/promises');
+const fsSync = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+const testDir = tmpdir.path;
+tmpdir.refresh();
+
+(async function run() {
+  // Add a file to already watching folder
+
+  const testsubdir = await fs.mkdtemp(testDir + path.sep);
+  const file = '1.txt';
+  const filePath = path.join(testsubdir, file);
+  const watcher = fs.watch(testsubdir, { recursive: true });
+
+  let interval;
+
+  process.on('exit', function() {
+    assert.ok(interval === null, 'watcher Object was not closed');
+  });
+
+  process.nextTick(common.mustCall(() => {
+    interval = setInterval(() => {
+      fsSync.writeFileSync(filePath, 'world');
+    }, 500);
+  }));
+
+  for await (const payload of watcher) {
+    const { eventType, filename } = payload;
+
+    assert.ok(eventType === 'change' || eventType === 'rename');
+
+    if (filename === file) {
+      break;
+    }
+  }
+
+  clearInterval(interval);
+  interval = null;
+})().then(common.mustCall());
+
+(async function() {
+  // Test that aborted AbortSignal are reported.
+  const testsubdir = await fs.mkdtemp(testDir + path.sep);
+  const error = new Error();
+  const watcher = fs.watch(testsubdir, { recursive: true, signal: AbortSignal.abort(error) });
+  await assert.rejects(async () => {
+    // eslint-disable-next-line no-unused-vars
+    for await (const _ of watcher);
+  }, { code: 'ABORT_ERR', cause: error });
+})().then(common.mustCall());
+
+(async function() {
+  // Test that with AbortController.
+  const testsubdir = await fs.mkdtemp(testDir + path.sep);
+  const file = '2.txt';
+  const filePath = path.join(testsubdir, file);
+  const error = new Error();
+  const ac = new AbortController();
+  const watcher = fs.watch(testsubdir, { recursive: true, signal: ac.signal });
+  let interval;
+  process.on('exit', function() {
+    assert.ok(interval === null, 'watcher Object was not closed');
+  });
+  process.nextTick(common.mustCall(() => {
+    interval = setInterval(() => {
+      fsSync.writeFileSync(filePath, 'world');
+    }, 50);
+    ac.abort(error);
+  }));
+  await assert.rejects(async () => {
+    for await (const { eventType } of watcher) {
+      assert.ok(eventType === 'change' || eventType === 'rename');
+    }
+  }, { code: 'ABORT_ERR', cause: error });
+  clearInterval(interval);
+  interval = null;
+})().then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js b/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js
new file mode 100644
index 000000000000..b1afffb659d4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js
@@ -0,0 +1,111 @@
+'use strict';
+
+const common = require('../common');
+const { setTimeout } = require('timers/promises');
+
+if (common.isIBMi)
+  common.skip('IBMi does not support `fs.watch()`');
+
+// fs-watch on folders have limited capability in AIX.
+// The testcase makes use of folder watching, and causes
+// hang. This behavior is documented. Skip this for AIX.
+
+if (common.isAIX)
+  common.skip('folder watch capability is limited in AIX.');
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const tmpdir = require('../common/tmpdir');
+const testDir = tmpdir.path;
+tmpdir.refresh();
+
+(async () => {
+  // Add a recursive symlink to the parent folder
+
+  const testDirectory = fs.mkdtempSync(testDir + path.sep);
+
+  // Do not use `testDirectory` as base. It will hang the tests.
+  const rootDirectory = path.join(testDirectory, 'test-1');
+  fs.mkdirSync(rootDirectory);
+
+  const filePath = path.join(rootDirectory, 'file.txt');
+
+  const symlinkFolder = path.join(rootDirectory, 'symlink-folder');
+  fs.symlinkSync(rootDirectory, symlinkFolder);
+
+  if (common.isMacOS) {
+    // On macOS delay watcher start to avoid leaking previous events.
+    // Refs: https://github.com/libuv/libuv/pull/4503
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  const watcher = fs.watch(rootDirectory, { recursive: true });
+  let watcherClosed = false;
+  watcher.on('change', common.mustCallAtLeast((event, filename) => {
+    assert.ok(event === 'rename', `Received ${event}`);
+    assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(filePath), `Received ${filename}`);
+
+    if (filename === path.basename(filePath)) {
+      watcher.close();
+      watcherClosed = true;
+    }
+  }));
+
+  await setTimeout(common.platformTimeout(100));
+  fs.writeFileSync(filePath, 'world');
+
+  process.once('exit', function() {
+    assert(watcherClosed, 'watcher Object was not closed');
+  });
+})().then(common.mustCall());
+
+(async () => {
+  // This test checks how a symlink to outside the tracking folder can trigger change
+  // tmp/sub-directory/tracking-folder/symlink-folder -> tmp/sub-directory
+
+  const rootDirectory = fs.mkdtempSync(testDir + path.sep);
+
+  const subDirectory = path.join(rootDirectory, 'sub-directory');
+  fs.mkdirSync(subDirectory);
+
+  const trackingSubDirectory = path.join(subDirectory, 'tracking-folder');
+  fs.mkdirSync(trackingSubDirectory);
+
+  const symlinkFolder = path.join(trackingSubDirectory, 'symlink-folder');
+  fs.symlinkSync(subDirectory, symlinkFolder);
+
+  const forbiddenFile = path.join(subDirectory, 'forbidden.txt');
+  const acceptableFile = path.join(trackingSubDirectory, 'acceptable.txt');
+
+  if (common.isMacOS) {
+    // On macOS delay watcher start to avoid leaking previous events.
+    // Refs: https://github.com/libuv/libuv/pull/4503
+    await setTimeout(common.platformTimeout(100));
+  }
+
+  const watcher = fs.watch(trackingSubDirectory, { recursive: true });
+  let watcherClosed = false;
+  watcher.on('change', common.mustCallAtLeast((event, filename) => {
+    // macOS will only change the following events:
+    // { event: 'rename', filename: 'symlink-folder' }
+    // { event: 'rename', filename: 'acceptable.txt' }
+    assert.ok(event === 'rename', `Received ${event}`);
+    assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(acceptableFile), `Received ${filename}`);
+
+    if (filename === path.basename(acceptableFile)) {
+      watcher.close();
+      watcherClosed = true;
+    }
+  }));
+
+  await setTimeout(common.platformTimeout(100));
+  fs.writeFileSync(forbiddenFile, 'world');
+  await setTimeout(common.platformTimeout(100));
+  fs.writeFileSync(acceptableFile, 'acceptable');
+
+  process.once('exit', function() {
+    assert(watcherClosed, 'watcher Object was not closed');
+  });
+})().then(common.mustCall());
diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js
index 3449db8e59ad..88db6acea6e5 100644
--- a/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js
+++ b/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js
@@ -33,7 +33,7 @@ tmpdir.refresh();
   const watcher = fs.watch(filePath, { recursive: true });
   let watcherClosed = false;
   let interval;
-  watcher.on('change', function(event, filename) {
+  watcher.on('change', common.mustCall((event, filename) => {
     assert.strictEqual(event, 'change');
 
     if (filename === path.basename(filePath)) {
@@ -42,7 +42,7 @@ tmpdir.refresh();
       watcher.close();
       watcherClosed = true;
     }
-  });
+  }));
 
   interval = setInterval(() => {
     fs.writeFileSync(filePath, 'world');
diff --git a/test/js/node/test/parallel/test-fs-watch-stop-async.js b/test/js/node/test/parallel/test-fs-watch-stop-async.js
index 64995730b6b9..395fdc0d1890 100644
--- a/test/js/node/test/parallel/test-fs-watch-stop-async.js
+++ b/test/js/node/test/parallel/test-fs-watch-stop-async.js
@@ -1,9 +1,10 @@
 'use strict';
 const common = require('../common');
+
 const assert = require('assert');
 const fs = require('fs');
 
-const watch = fs.watchFile(__filename, () => {});
+const watch = fs.watchFile(__filename, common.mustNotCall());
 let triggered;
 const listener = common.mustCall(() => {
   triggered = true;
@@ -12,8 +13,8 @@ const listener = common.mustCall(() => {
 triggered = false;
 watch.once('stop', listener);  // Should trigger.
 watch.stop();
-assert.equal(triggered, false);
-setImmediate(() => {
-  assert.equal(triggered, true);
+assert.strictEqual(triggered, false);
+setImmediate(common.mustCall(() => {
+  assert.strictEqual(triggered, true);
   watch.removeListener('stop', listener);
-});
\ No newline at end of file
+}));
diff --git a/test/js/node/test/parallel/test-fs-watchfile.js b/test/js/node/test/parallel/test-fs-watchfile.js
index 6fabedd67e8e..e1fd3f4c3433 100644
--- a/test/js/node/test/parallel/test-fs-watchfile.js
+++ b/test/js/node/test/parallel/test-fs-watchfile.js
@@ -42,10 +42,14 @@ const expectedStatObject = new fs.Stats(
   0,                                        // ino
   0,                                        // size
   0,                                        // blocks
-  Date.UTC(1970, 0, 1, 0, 0, 0),            // atime
-  Date.UTC(1970, 0, 1, 0, 0, 0),            // mtime
-  Date.UTC(1970, 0, 1, 0, 0, 0),            // ctime
-  Date.UTC(1970, 0, 1, 0, 0, 0)             // birthtime
+  0,                                        // atimeS
+  0,                                        // atimeNs
+  0,                                        // mtimeS
+  0,                                        // mtimeNs
+  0,                                        // ctime
+  0,                                        // ctimeNs
+  0,                                        // birthtime
+  0,                                        // birthtimeNs
 );
 
 tmpdir.refresh();
@@ -94,9 +98,7 @@ if (common.isLinux || common.isMacOS || common.isWindows) {
     }));
 
     const interval = setInterval(() => {
-      fs.writeFile(path.join(dir, 'foo.txt'), 'foo', common.mustCall((err) => {
-        if (err) assert.fail(err);
-      }));
+      fs.writeFile(path.join(dir, 'foo.txt'), 'foo', common.mustSucceed());
     }, 1);
   }
 
@@ -109,4 +111,4 @@ if (common.isLinux || common.isMacOS || common.isWindows) {
       doWatch();
     }
   }));
-}
\ No newline at end of file
+}
diff --git a/test/js/node/test/parallel/test-fs-write-optional-params.js b/test/js/node/test/parallel/test-fs-write-optional-params.js
index eebc1cc88c95..3eda9141ee02 100644
--- a/test/js/node/test/parallel/test-fs-write-optional-params.js
+++ b/test/js/node/test/parallel/test-fs-write-optional-params.js
@@ -108,5 +108,5 @@ async function runTests(fd) {
 }
 
 fs.open(destInvalid, 'w+', common.mustSucceed(async (fd) => {
-  runTests(fd).then(common.mustCall(() => fs.close(fd, common.mustSucceed())));
+  runTests(fd).then(common.mustCall(() => { fs.close(fd, common.mustSucceed()); }));
 }));
diff --git a/test/js/node/test/parallel/test-fs-write-stream-change-open.js b/test/js/node/test/parallel/test-fs-write-stream-change-open.js
index b95abb1cb34c..b62acbd4d7f6 100644
--- a/test/js/node/test/parallel/test-fs-write-stream-change-open.js
+++ b/test/js/node/test/parallel/test-fs-write-stream-change-open.js
@@ -20,7 +20,7 @@
 // USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 'use strict';
-require('../common');
+const common = require('../common');
 const assert = require('assert');
 const fs = require('fs');
 
@@ -41,12 +41,12 @@ fs.open = function() {
   return _fs_open.apply(fs, arguments);
 };
 
-fs.close = function(fd) {
+fs.close = common.mustCall(function(fd) {
   assert.ok(fd, 'fs.close must not be called with an undefined fd.');
   fs.close = _fs_close;
   fs.open = _fs_open;
   fs.closeSync(fd);
-};
+});
 
 stream.write('foo');
 stream.end();
diff --git a/test/js/node/test/parallel/test-fs-write-stream-eagain.mjs b/test/js/node/test/parallel/test-fs-write-stream-eagain.mjs
new file mode 100644
index 000000000000..935c5a0ae6c4
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-write-stream-eagain.mjs
@@ -0,0 +1,39 @@
+import * as common from '../common/index.mjs';
+import tmpdir from '../common/tmpdir.js';
+import assert from 'node:assert';
+import fs from 'node:fs';
+import { describe, it, mock } from 'node:test';
+import { finished } from 'node:stream/promises';
+
+tmpdir.refresh();
+const file = tmpdir.resolve('writeStreamEAGAIN.txt');
+const errorWithEAGAIN = (fd, buffer, offset, length, position, callback) => {
+  callback(Object.assign(new Error(), { code: 'EAGAIN' }), 0, buffer);
+};
+
+describe('WriteStream EAGAIN', { concurrency: !process.env.TEST_PARALLEL }, () => {
+  it('_write', async () => {
+    const mockWrite = mock.fn(fs.write);
+    mockWrite.mock.mockImplementationOnce(errorWithEAGAIN);
+    const stream = fs.createWriteStream(file, {
+      fs: {
+        open: common.mustCall(fs.open),
+        write: mockWrite,
+        close: common.mustCall(fs.close),
+      }
+    });
+    stream.end('foo');
+    stream.on('close', common.mustCall());
+    stream.on('error', common.mustNotCall());
+    await finished(stream);
+    assert.strictEqual(mockWrite.mock.callCount(), 2);
+    assert.strictEqual(fs.readFileSync(file, 'utf8'), 'foo');
+  });
+
+  it('_write', async () => {
+    const stream = fs.createWriteStream(file);
+    mock.getter(stream, 'destroyed', () => true);
+    stream.end('foo');
+    await finished(stream).catch(common.mustCall());
+  });
+});
diff --git a/test/js/node/test/parallel/test-fs-write-stream-encoding.js b/test/js/node/test/parallel/test-fs-write-stream-encoding.js
index f06fae923c68..fa86c1ae8b77 100644
--- a/test/js/node/test/parallel/test-fs-write-stream-encoding.js
+++ b/test/js/node/test/parallel/test-fs-write-stream-encoding.js
@@ -1,5 +1,5 @@
 'use strict';
-require('../common');
+const common = require('../common');
 const assert = require('assert');
 const fixtures = require('../common/fixtures');
 const fs = require('fs');
@@ -21,15 +21,15 @@ const dummyWriteStream = fs.createWriteStream(dummyPath, {
   encoding: firstEncoding
 });
 
-exampleReadStream.pipe(dummyWriteStream).on('finish', function() {
+exampleReadStream.pipe(dummyWriteStream).on('finish', common.mustCall(() => {
   const assertWriteStream = new stream.Writable({
-    write: function(chunk, enc, next) {
+    write: common.mustCall((chunk, enc, next) => {
       const expected = Buffer.from('xyz\n');
       assert(chunk.equals(expected));
-    }
+    }),
   });
   assertWriteStream.setDefaultEncoding(secondEncoding);
   fs.createReadStream(dummyPath, {
     encoding: secondEncoding
   }).pipe(assertWriteStream);
-});
+}));
diff --git a/test/js/node/test/parallel/test-fs-write-stream-err.js b/test/js/node/test/parallel/test-fs-write-stream-err.js
index 003f315a3b71..4343bb4c01d5 100644
--- a/test/js/node/test/parallel/test-fs-write-stream-err.js
+++ b/test/js/node/test/parallel/test-fs-write-stream-err.js
@@ -68,10 +68,10 @@ stream.on('error', common.mustCall(function(err_) {
 }));
 
 
-stream.write(Buffer.allocUnsafe(256), function() {
+stream.write(Buffer.allocUnsafe(256), common.mustCall(() => {
   console.error('first cb');
   stream.write(Buffer.allocUnsafe(256), common.mustCall(function(err_) {
     console.error('second cb');
     assert.strictEqual(err_, err);
   }));
-});
+}));
diff --git a/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js b/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js
index 93c52e96cb35..2bbe82a83555 100644
--- a/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js
+++ b/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js
@@ -1,5 +1,5 @@
 'use strict';
-require('../common');
+const common = require('../common');
 const assert = require('assert');
 const fs = require('fs');
 
@@ -14,7 +14,7 @@ fs.createWriteStream(example, null).end();
 fs.createWriteStream(example, 'utf8').end();
 fs.createWriteStream(example, { encoding: 'utf8' }).end();
 
-const createWriteStreamErr = (path, opt) => {
+const createWriteStreamErr = common.mustCall((path, opt) => {
   assert.throws(
     () => {
       fs.createWriteStream(path, opt);
@@ -23,7 +23,7 @@ const createWriteStreamErr = (path, opt) => {
       code: 'ERR_INVALID_ARG_TYPE',
       name: 'TypeError'
     });
-};
+}, 4);
 
 createWriteStreamErr(example, 123);
 createWriteStreamErr(example, 0);
diff --git a/test/js/node/test/parallel/test-fs-write-stream.js b/test/js/node/test/parallel/test-fs-write-stream.js
index a3dccf7cdcb7..552bb3d1faa5 100644
--- a/test/js/node/test/parallel/test-fs-write-stream.js
+++ b/test/js/node/test/parallel/test-fs-write-stream.js
@@ -34,11 +34,11 @@ tmpdir.refresh();
   const stream = fs.WriteStream(file);
   const _fs_close = fs.close;
 
-  fs.close = function(fd) {
+  fs.close = common.mustCall(function(fd) {
     assert.ok(fd, 'fs.close must not be called without an undefined fd.');
     fs.close = _fs_close;
     fs.closeSync(fd);
-  };
+  });
   stream.destroy();
 }
 
diff --git a/test/js/node/test/parallel/test-fs-write-sync-optional-params.js b/test/js/node/test/parallel/test-fs-write-sync-optional-params.js
new file mode 100644
index 000000000000..61a71ac07cd8
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-write-sync-optional-params.js
@@ -0,0 +1,104 @@
+'use strict';
+
+const common = require('../common');
+
+// This test ensures that fs.writeSync accepts "named parameters" object
+// and doesn't interpret objects as strings
+
+const assert = require('assert');
+const fs = require('fs');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const dest = tmpdir.resolve('tmp.txt');
+const buffer = Buffer.from('zyx');
+
+function testInvalid(dest, expectedCode, ...bufferAndOptions) {
+  if (bufferAndOptions.length >= 2) {
+    bufferAndOptions[1] = common.mustNotMutateObjectDeep(bufferAndOptions[1]);
+  }
+  let fd;
+  try {
+    fd = fs.openSync(dest, 'w+');
+    assert.throws(
+      () => fs.writeSync(fd, ...bufferAndOptions),
+      { code: expectedCode });
+  } finally {
+    if (fd != null) fs.closeSync(fd);
+  }
+}
+
+function testValid(dest, buffer, options) {
+  const length = options?.length;
+  let fd, bytesWritten, bytesRead;
+
+  try {
+    fd = fs.openSync(dest, 'w');
+    bytesWritten = fs.writeSync(fd, buffer, options);
+  } finally {
+    if (fd != null) fs.closeSync(fd);
+  }
+
+  try {
+    fd = fs.openSync(dest, 'r');
+    bytesRead = fs.readSync(fd, buffer, options);
+  } finally {
+    if (fd != null) fs.closeSync(fd);
+  }
+
+  assert.ok(bytesWritten >= bytesRead);
+  if (length !== undefined && length !== null) {
+    assert.strictEqual(bytesWritten, length);
+    assert.strictEqual(bytesRead, length);
+  }
+}
+
+{
+  // Test if second argument is not wrongly interpreted as string or options
+  for (const badBuffer of [
+    undefined, null, true, 42, 42n, Symbol('42'), NaN, [], () => {},
+    common.mustNotCall(),
+    common.mustNotMutateObjectDeep({}),
+    {},
+    { buffer: 'amNotParam' },
+    { string: 'amNotParam' },
+    { buffer: new Uint8Array(1) },
+    { buffer: new Uint8Array(1).buffer },
+    Promise.resolve(new Uint8Array(1)),
+    new Date(),
+    new String('notPrimitive'),
+    { toString() { return 'amObject'; } },
+    { [Symbol.toPrimitive]: (hint) => 'amObject' },
+  ]) {
+    testInvalid(dest, 'ERR_INVALID_ARG_TYPE', common.mustNotMutateObjectDeep(badBuffer));
+  }
+
+  // First argument (buffer or string) is mandatory
+  testInvalid(dest, 'ERR_INVALID_ARG_TYPE');
+
+  // Various invalid options
+  testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 });
+  testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 });
+  testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 });
+  testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 });
+  testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 });
+  testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: false });
+  testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: true });
+
+  // Test compatibility with fs.readSync counterpart with reused options
+  for (const options of [
+    undefined,
+    null,
+    {},
+    { length: 1 },
+    { position: 5 },
+    { length: 1, position: 5 },
+    { length: 1, position: -1, offset: 2 },
+    { length: null },
+    { position: null },
+    { offset: 1 },
+  ]) {
+    testValid(dest, buffer, common.mustNotMutateObjectDeep(options));
+  }
+}
diff --git a/test/js/node/test/parallel/test-fs-writestream-open-write.js b/test/js/node/test/parallel/test-fs-writestream-open-write.js
index af02d90ae6ef..f73697edf896 100644
--- a/test/js/node/test/parallel/test-fs-writestream-open-write.js
+++ b/test/js/node/test/parallel/test-fs-writestream-open-write.js
@@ -2,7 +2,7 @@
 
 const common = require('../common');
 const tmpdir = require('../common/tmpdir');
-const { strictEqual } = require('assert');
+const assert = require('assert');
 const fs = require('fs');
 
 // Regression test for https://github.com/nodejs/node/issues/51993
@@ -23,6 +23,6 @@ w.on('open', common.mustCall(() => {
 }));
 
 w.on('close', common.mustCall(() => {
-  strictEqual(fs.readFileSync(file, 'utf8'), 'helloworld');
+  assert.strictEqual(fs.readFileSync(file, 'utf8'), 'helloworld');
   fs.unlinkSync(file);
 }));
diff --git a/test/js/node/test/parallel/test-fs-writesync-crash.js b/test/js/node/test/parallel/test-fs-writesync-crash.js
new file mode 100644
index 000000000000..d94360d892ad
--- /dev/null
+++ b/test/js/node/test/parallel/test-fs-writesync-crash.js
@@ -0,0 +1,40 @@
+'use strict';
+
+require('../common');
+
+const {
+  writeSync,
+  writeFileSync,
+  chmodSync,
+  openSync,
+} = require('node:fs');
+
+const assert = require('node:assert');
+
+// If a file's mode change after it is opened but before it is written to,
+// and the Object.prototype is manipulated to throw an error when the errno
+// or fd property is set or accessed, then the writeSync call would crash
+// the process. This test verifies that the error is properly propagated
+// instead.
+
+const tmpdir = require('../common/tmpdir');
+console.log(tmpdir.path);
+tmpdir.refresh();
+const path = `${tmpdir.path}/foo`;
+writeFileSync(path, '');
+
+// Do this after calling tmpdir.refresh() or that call will fail
+// before we get to the part we want to test.
+const error = new Error();
+Object.defineProperty(Object.prototype, 'errno', {
+  __proto__: null,
+  set() {
+    throw error;
+  },
+  get() { return 0; }
+});
+
+const fd = openSync(path);
+chmodSync(path, 0o600);
+
+assert.throws(() => writeSync(fd, 'test'), error);
diff --git a/test/js/node/test/parallel/test-permission-fs-supported.js b/test/js/node/test/parallel/test-permission-fs-supported.js
index 5797e191cd20..a6cf9146c629 100644
--- a/test/js/node/test/parallel/test-permission-fs-supported.js
+++ b/test/js/node/test/parallel/test-permission-fs-supported.js
@@ -38,6 +38,7 @@ const supportedApis = [
   ...syncAndAsyncAPI('open'),
   'openAsBlob',
   ...syncAndAsyncAPI('mkdtemp'),
+  'mkdtempDisposableSync',
   ...syncAndAsyncAPI('readdir'),
   ...syncAndAsyncAPI('readFile'),
   ...syncAndAsyncAPI('readlink'),
diff --git a/test/js/node/test/sequential/test-fs-opendir-recursive.js b/test/js/node/test/sequential/test-fs-opendir-recursive.js
index 494e5591491b..26d4d8e15050 100644
--- a/test/js/node/test/sequential/test-fs-opendir-recursive.js
+++ b/test/js/node/test/sequential/test-fs-opendir-recursive.js
@@ -128,17 +128,17 @@ for (let i = 0; i < expected.length; i++) {
 }
 
 function getDirentPath(dirent) {
-  return pathModule.relative(testDir, pathModule.join(dirent.path, dirent.name));
+  return pathModule.relative(testDir, pathModule.join(dirent.parentPath, dirent.name));
 }
 
 function assertDirents(dirents) {
   assert.strictEqual(dirents.length, expected.length);
   dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
   assert.deepStrictEqual(
-    dirents.map((dirent) => {
+    dirents.map(common.mustCallAtLeast((dirent) => {
       assert(dirent instanceof fs.Dirent);
       return getDirentPath(dirent);
-    }),
+    })),
     expected
   );
 }
diff --git a/test/js/node/test/sequential/test-fs-readdir-recursive.js b/test/js/node/test/sequential/test-fs-readdir-recursive.js
index 277557383778..15bc75c6c928 100644
--- a/test/js/node/test/sequential/test-fs-readdir-recursive.js
+++ b/test/js/node/test/sequential/test-fs-readdir-recursive.js
@@ -127,18 +127,18 @@ for (let i = 0; i < expected.length; i++) {
 }
 
 function getDirentPath(dirent) {
-  return pathModule.relative(readdirDir, pathModule.join(dirent.path, dirent.name));
+  return pathModule.relative(readdirDir, pathModule.join(dirent.parentPath, dirent.name));
 }
 
 function assertDirents(dirents) {
   assert.strictEqual(dirents.length, expected.length);
   dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
   assert.deepStrictEqual(
-    dirents.map((dirent) => {
+    dirents.map(common.mustCallAtLeast((dirent) => {
       assert(dirent instanceof fs.Dirent);
       assert.notStrictEqual(dirent.name, undefined);
       return getDirentPath(dirent);
-    }),
+    })),
     expected
   );
 }
diff --git a/test/js/node/test/sequential/test-fs-watch.js b/test/js/node/test/sequential/test-fs-watch.js
new file mode 100644
index 000000000000..8db27a79e33d
--- /dev/null
+++ b/test/js/node/test/sequential/test-fs-watch.js
@@ -0,0 +1,180 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+const common = require('../common');
+if (common.isIBMi)
+  common.skip('IBMi does not support fs.watch()');
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const tmpdir = require('../common/tmpdir');
+const { isMainThread } = require('worker_threads');
+
+if (!isMainThread) {
+  common.skip('process.chdir is not available in Workers');
+}
+
+const expectFilePath = common.isWindows ||
+                       common.isLinux ||
+                       common.isMacOS ||
+                       common.isAIX;
+
+const testDir = tmpdir.path;
+
+tmpdir.refresh();
+
+// Because macOS (and possibly other operating systems) can return a watcher
+// before it is actually watching, we need to repeat the operation to avoid
+// a race condition.
+function repeat(fn) {
+  setImmediate(fn);
+  const interval = setInterval(fn, 5000);
+  return interval;
+}
+
+{
+  const filepath = path.join(testDir, 'watch.txt');
+
+  fs.writeFileSync(filepath, 'hello');
+
+  const watcher = fs.watch(filepath);
+  watcher.on('change', common.mustCall(function(event, filename) {
+    assert.strictEqual(event, 'change');
+
+    if (expectFilePath) {
+      assert.strictEqual(filename, 'watch.txt');
+    }
+    clearInterval(interval);
+    watcher.close();
+  }));
+
+  const interval = repeat(() => { fs.writeFileSync(filepath, 'world'); });
+}
+
+{
+  const filepathAbs = path.join(testDir, 'hasOwnProperty');
+
+  process.chdir(testDir);
+
+  fs.writeFileSync(filepathAbs, 'howdy');
+
+  const watcher =
+    fs.watch('hasOwnProperty', common.mustCall(function(event, filename) {
+      assert.strictEqual(event, 'change');
+
+      if (expectFilePath) {
+        assert.strictEqual(filename, 'hasOwnProperty');
+      }
+      clearInterval(interval);
+      watcher.close();
+    }));
+
+  const interval = repeat(() => { fs.writeFileSync(filepathAbs, 'pardner'); });
+}
+
+{
+  const testsubdir = fs.mkdtempSync(testDir + path.sep);
+  const filepath = path.join(testsubdir, 'newfile.txt');
+
+  function doWatch() {
+    const watcher =
+      fs.watch(testsubdir, common.mustCall(function(event, filename) {
+        const renameEv = common.isSunOS || common.isAIX ? 'change' : 'rename';
+        assert.strictEqual(event, renameEv);
+        if (expectFilePath) {
+          assert.strictEqual(filename, 'newfile.txt');
+        } else {
+          assert.strictEqual(filename, null);
+        }
+        clearInterval(interval);
+        watcher.close();
+      }));
+
+    const interval = repeat(() => {
+      fs.rmSync(filepath, { force: true });
+      const fd = fs.openSync(filepath, 'w');
+      fs.closeSync(fd);
+    });
+  }
+
+  if (common.isMacOS) {
+    // On macOS delay watcher start to avoid leaking previous events.
+    // Refs: https://github.com/libuv/libuv/pull/4503
+    setTimeout(doWatch, common.platformTimeout(100));
+  } else {
+    doWatch();
+  }
+}
+
+// https://github.com/joyent/node/issues/2293 - non-persistent watcher should
+// not block the event loop
+{
+  fs.watch(__filename, { persistent: false }, common.mustNotCall());
+}
+
+// Whitebox test to ensure that wrapped FSEvent is safe
+// https://github.com/joyent/node/issues/6690
+{
+  if (common.isMacOS || common.isWindows) {
+    let oldhandle;
+    assert.throws(
+      () => {
+        const w = fs.watch(__filename, common.mustNotCall());
+        oldhandle = w._handle;
+        w._handle = { close: w._handle.close };
+        w.close();
+      },
+      {
+        name: 'Error',
+        code: 'ERR_INTERNAL_ASSERTION',
+        message: /^handle must be a FSEvent/,
+      }
+    );
+    oldhandle.close(); // clean up
+  }
+}
+
+{
+  if (common.isMacOS || common.isWindows) {
+    let oldhandle;
+    assert.throws(
+      () => {
+        const w = fs.watch(__filename, common.mustNotCall());
+        oldhandle = w._handle;
+        const protoSymbols =
+          Object.getOwnPropertySymbols(Object.getPrototypeOf(w));
+        const kFSWatchStart =
+          protoSymbols.find((val) => val.toString() === 'Symbol(kFSWatchStart)');
+        w._handle = {};
+        w[kFSWatchStart]();
+      },
+      {
+        name: 'Error',
+        code: 'ERR_INTERNAL_ASSERTION',
+        message: /^handle must be a FSEvent/,
+      }
+    );
+    oldhandle.close(); // clean up
+  }
+}
diff --git a/test/js/node/test_runner/node-test.test.ts b/test/js/node/test_runner/node-test.test.ts
index d94009e9b251..3e422ae5ae0d 100644
--- a/test/js/node/test_runner/node-test.test.ts
+++ b/test/js/node/test_runner/node-test.test.ts
@@ -72,3 +72,157 @@ async function runTests(filenames: string[]) {
   ]);
   return { exitCode, stdout, stderr };
 }
+
+describe("node:test mock", () => {
+  const { mock } = require("node:test");
+
+  test("mock.getter accepts the (object, methodName, options) overload", () => {
+    const obj = {
+      get prop() {
+        return "original";
+      },
+    };
+    // Passing an options object in the implementation slot must not clobber
+    // the getter flag.
+    const getter = mock.getter(obj, "prop", {});
+    expect(obj.prop).toBe("original");
+    expect(getter.mock.callCount()).toBe(1);
+    mock.restoreAll();
+  });
+
+  test("mock.setter accepts the (object, methodName, options) overload", () => {
+    let stored = "";
+    const obj = {
+      set prop(v: string) {
+        stored = v;
+      },
+    };
+    const setter = mock.setter(obj, "prop", {});
+    obj.prop = "x";
+    expect(stored).toBe("x");
+    expect(setter.mock.callCount()).toBe(1);
+    mock.restoreAll();
+  });
+
+  test("mock.getter rejects getter: false", () => {
+    const obj = {
+      get prop() {
+        return 1;
+      },
+    };
+    expect(() => mock.getter(obj, "prop", { getter: false })).toThrow(
+      expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }),
+    );
+  });
+
+  test("mock.method rejects getter and setter together", () => {
+    const obj = {
+      get prop() {
+        return 1;
+      },
+      set prop(_v) {},
+    };
+    expect(() => mock.method(obj, "prop", { getter: true, setter: true })).toThrow(
+      expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }),
+    );
+  });
+
+  test("mock.fn options.times reverts to the original after N calls", () => {
+    const original = () => "original";
+    const impl = () => "mocked";
+    const fn = mock.fn(original, impl, { times: 2 });
+    expect(fn()).toBe("mocked");
+    expect(fn()).toBe("mocked");
+    expect(fn()).toBe("original");
+    expect(fn.mock.callCount()).toBe(3);
+    mock.restoreAll();
+  });
+
+  test("mock.method options.times restores the method after N calls", () => {
+    const obj = {
+      value: 5,
+      addOne() {
+        return this.value + 1;
+      },
+    };
+    mock.method(obj, "addOne", () => 100, { times: 1 });
+    expect(obj.addOne()).toBe(100);
+    expect(obj.addOne()).toBe(6);
+    mock.restoreAll();
+  });
+
+  test("mock.fn options.times is validated", () => {
+    expect(() => mock.fn(() => {}, { times: 0 })).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" }));
+    expect(() => mock.fn(() => {}, { times: 1.5 })).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" }));
+  });
+
+  test("mock.restoreAll makes bare mock.fn mocks call their original again", () => {
+    const fn = mock.fn(
+      () => "original",
+      () => "mocked",
+    );
+    expect(fn()).toBe("mocked");
+    mock.restoreAll();
+    expect(fn()).toBe("original");
+  });
+});
+
+describe("node:test mock tracker semantics", () => {
+  const { mock } = require("node:test");
+
+  test("restoreAll keeps mocks associated; reset disassociates", () => {
+    // mirrors observed node behavior exactly
+    const f = mock.fn(
+      () => "orig",
+      () => "mocked",
+    );
+    expect(f()).toBe("mocked");
+    mock.restoreAll();
+    expect(f()).toBe("orig");
+    // still tracked after restoreAll: reset() reverts a re-installed
+    // implementation again
+    f.mock.mockImplementation(() => "again");
+    expect(f()).toBe("again");
+    mock.reset();
+    expect(f()).toBe("orig");
+    // after reset() the context is disassociated: restoreAll no longer
+    // touches it
+    f.mock.mockImplementation(() => "post-reset");
+    mock.restoreAll();
+    expect(f()).toBe("post-reset");
+    mock.reset();
+  });
+
+  test("queued once-implementations survive restoreAll like node", () => {
+    const g = mock.fn(
+      () => "g-orig",
+      () => "g-mocked",
+    );
+    g.mock.mockImplementationOnce(() => "g-once", 1);
+    mock.restoreAll();
+    expect([g(), g(), g()]).toEqual(["g-orig", "g-once", "g-orig"]);
+    mock.reset();
+  });
+
+  test("mock.method validates a non-object options argument", () => {
+    const obj = {
+      foo() {},
+    };
+    expect(() => mock.method(obj, "foo", () => {}, 5)).toThrow(
+      expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" }),
+    );
+  });
+});
+
+test("the call record is pushed after the implementation runs, like node", () => {
+  const { mock } = require("node:test");
+  let inside = -1;
+  const f = mock.fn(function () {
+    inside = f.mock.callCount();
+    return 1;
+  });
+  f();
+  expect(inside).toBe(0);
+  expect(f.mock.callCount()).toBe(1);
+  mock.reset();
+});
diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts
index 189197d21a90..22da3333b4b0 100644
--- a/test/js/node/watch/fs.watch.test.ts
+++ b/test/js/node/watch/fs.watch.test.ts
@@ -654,6 +654,33 @@ describe("fs.promises.watch", () => {
     })();
   });
 
+  test("Signal aborted before creating the watcher does not keep the process alive", async () => {
+    const filepath = path.join(testDir, "abort.txt");
+    // If a native watcher were created for a pre-aborted signal, nothing
+    // would ever close it and the process would never exit.
+    await using proc = Bun.spawn({
+      cmd: [
+        bunExe(),
+        "-e",
+        `const fs = require("node:fs");
+        const signal = AbortSignal.abort();
+        (async () => {
+          try {
+            for await (const _ of fs.promises.watch(${JSON.stringify(filepath)}, { signal }));
+            throw new Error("expected AbortError");
+          } catch (e) {
+            if (e.name !== "AbortError") throw e;
+          }
+        })();`,
+      ],
+      env: bunEnv,
+      stderr: "pipe",
+    });
+    const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
+    expect(stderr).toBe("");
+    expect(exitCode).toBe(0);
+  });
+
   test("should work with symlink -> symlink -> dir", async () => {
     const filepath = path.join(testDir, "sym-symlink-indirect");
     const dest = path.join(testDir, "sym-symlink-dest");
diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts
index 555c205c6fc7..9d4958118148 100644
--- a/test/js/node/worker_threads/worker_threads.test.ts
+++ b/test/js/node/worker_threads/worker_threads.test.ts
@@ -1,4 +1,4 @@
-import { bunEnv, bunExe } from "harness";
+import { bunEnv, bunExe, tmpdirSync } from "harness";
 import { once } from "node:events";
 import fs from "node:fs";
 import { join, relative, resolve } from "node:path";
@@ -487,3 +487,144 @@ describe("getHeapSnapshot", () => {
     worker.postMessage(0);
   });
 });
+
+test("failed Worker construction restores transferred FileHandles", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const fh = await fs.promises.open(file, "r");
+  // Non-cloneable workerData makes the WebWorker constructor throw after the
+  // FileHandle has already been neutered by the transfer machinery; the fd
+  // must be restored so the handle stays usable.
+  expect(() => {
+    new Worker(file, { transferList: [fh as any], workerData: { fh, bad: () => {} } } as any);
+  }).toThrow();
+  const { bytesRead } = await fh.read(Buffer.alloc(5), 0, 5, 0);
+  expect(bytesRead).toBe(5);
+  await fh.close();
+});
+
+test("partially transferred FileHandles are restored when a later transfer throws", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const fh1 = await fs.promises.open(file, "r");
+  const fh2 = await fs.promises.open(file, "r");
+  const pending = fh2.read(Buffer.alloc(5), 0, 5, 0); // fh2 is in use -> its transfer throws
+  expect(() => {
+    new Worker(file, { transferList: [fh1 as any, fh2 as any], workerData: { fh1, fh2 } } as any);
+  }).toThrow(expect.objectContaining({ name: "DataCloneError" }));
+  await pending;
+  const { bytesRead } = await fh1.read(Buffer.alloc(5), 0, 5, 0);
+  expect(bytesRead).toBe(5);
+  await fh1.close();
+  await fh2.close();
+});
+
+test("a FileHandle referenced twice in workerData deserializes to one instance", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const script = join(dir, "w.mjs");
+  fs.writeFileSync(
+    script,
+    `import { workerData, parentPort } from "worker_threads";
+     const { a, b } = workerData;
+     const same = a === b;
+     await a.close();
+     // b is the same handle, so it must be closed too (no stale second
+     // instance wrapping an already-closed fd)
+     const closed = b.fd === -1;
+     parentPort.postMessage({ same, closed });`,
+  );
+  const fh = await fs.promises.open(file, "r");
+  const worker = new Worker(script, { workerData: { a: fh, b: fh }, transferList: [fh as any] } as any);
+  const [message] = await once(worker, "message");
+  await worker.terminate();
+  expect(message).toEqual({ same: true, closed: true });
+});
+
+test("duplicate FileHandle transferList entries throw DataCloneError and roll back", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const fh = await fs.promises.open(file, "r");
+  expect(() => {
+    new Worker(file, { workerData: { fh }, transferList: [fh as any, fh as any] } as any);
+  }).toThrow(expect.objectContaining({ name: "DataCloneError" }));
+  // like node, the handle is still usable after the rejected transfer
+  const { bytesRead } = await fh.read(Buffer.alloc(5), 0, 5, 0);
+  expect(bytesRead).toBe(5);
+  await fh.close();
+});
+
+test("a FileHandle in transferList but not in workerData is detached without leaking", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const script = join(dir, "noop.mjs");
+  fs.writeFileSync(script, `import { parentPort } from "worker_threads"; parentPort.postMessage("ok");`);
+  const fh = await fs.promises.open(file, "r");
+  const fd = fh.fd;
+  const ino = fs.fstatSync(fd).ino;
+  const worker = new Worker(script, { workerData: {}, transferList: [fh as any] } as any);
+  const [message] = await once(worker, "message");
+  expect(message).toBe("ok");
+  await worker.terminate();
+  // the parent handle is neutered like node...
+  expect(fh.fd).toBe(-1);
+  // ...and the orphaned fd was closed (not leaked). The number may have been
+  // recycled by the worker machinery in the meantime, so accept either EBADF
+  // or a descriptor that no longer refers to our file.
+  let closedOrRecycled = false;
+  try {
+    closedOrRecycled = fs.fstatSync(fd).ino !== ino;
+  } catch (e: any) {
+    closedOrRecycled = e.code === "EBADF";
+  }
+  expect(closedOrRecycled).toBe(true);
+});
+
+test("failed construction restores an unreferenced transferred FileHandle intact", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const fh = await fs.promises.open(file, "r");
+  // workerData is non-cloneable, so WebWorker construction throws *after*
+  // the handle was neutered; the rollback must hand back a live fd, not a
+  // number that was already closed by the orphan-fd cleanup.
+  expect(() => {
+    new Worker(file, { workerData: () => {}, transferList: [fh as any] } as any);
+  }).toThrow();
+  const { bytesRead } = await fh.read(Buffer.alloc(5), 0, 5, 0);
+  expect(bytesRead).toBe(5);
+  await fh.close();
+});
+
+test("FileHandles nested in Map and Set workerData are transferred", async () => {
+  const dir = tmpdirSync("worker-fh-transfer");
+  const file = join(dir, "x.txt");
+  fs.writeFileSync(file, "hello");
+  const script = join(dir, "ms.mjs");
+  fs.writeFileSync(
+    script,
+    `import { workerData, parentPort } from "worker_threads";
+     const m = workerData.m.get("h");
+     const s = [...workerData.s][0];
+     const sameInstance = m === s;
+     const { buffer, bytesRead } = await m.read(Buffer.alloc(5), 0, 5, 0);
+     parentPort.postMessage({ sameInstance, text: buffer.toString("utf8", 0, bytesRead) });
+     await m.close();`,
+  );
+  const fh = await fs.promises.open(file, "r");
+  const worker = new Worker(script, {
+    workerData: { m: new Map([["h", fh]]), s: new Set([fh]) },
+    transferList: [fh as any],
+  } as any);
+  const [message] = await once(worker, "message");
+  await worker.terminate();
+  // parent side is neutered, worker read through the Map entry, and the Map
+  // and Set entries deserialized to the same single instance
+  expect(fh.fd).toBe(-1);
+  expect(message).toEqual({ sameInstance: true, text: "hello" });
+});