Skip to content

feat: allow injecting custom process implementation for better testability#1248

Merged
streamich merged 2 commits intomasterfrom
copilot/allow-injecting-custom-process-implementation
Mar 21, 2026
Merged

feat: allow injecting custom process implementation for better testability#1248
streamich merged 2 commits intomasterfrom
copilot/allow-injecting-custom-process-implementation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 21, 2026

memfs reads directly from the global Node process object (cwd(), platform, getuid()), making controlled testing without mutating globals impossible. This adds first-class support for injecting a custom process-like object at the fs instance level.

API changes

memfs() — second argument now accepts string | MemfsOptions (string form is backward-compatible):

const { fs } = memfs({ '/app/config.json': '{}' }, {
  process: {
    cwd: () => '/app',
    platform: 'win32',
    getuid: () => 1000,
    getgid: () => 1000,
    emitWarning: () => {},
    env: {},
  },
});

Volume.fromJSON / Volume.fromNestedJSON — accept optional { process?: IProcess } third argument:

const vol = Volume.fromJSON({ 'file.txt': 'hello' }, undefined, { process: fakeProcess });

Superblock constructor — accepts { process?: IProcess }:

const core = new Superblock({ process: fakeProcess });
const vol = new Volume(core);

Internal changes

  • Superblock: stores this.process (defaults to global singleton); createNode() reads uid/gid from it; walk() uses it for platform-specific error codes and access checks; fromJSON() uses this.process.cwd() as the default cwd.
  • Node: constructor accepts optional uid/gid params so Superblock.createNode() can seed ownership from the injected process.
  • IProcess is now exported from @jsonjoy.com/fs-core and re-exported from memfs.

cwd resolution precedence in memfs()

call form cwd used
memfs(json) '/' (unchanged default)
memfs(json, '/path') '/path'
memfs(json, { cwd: '/path' }) '/path'
memfs(json, { process }) process.cwd()
memfs(json, { cwd: '/path', process }) '/path'
Original prompt

This section details on the original issue you should resolve

<issue_title>Allow injecting custom process implementation for better testability</issue_title>
<issue_description>## Problem

memfs reads directly from the global Node process object (e.g., cwd(), platform, getuid()), which makes it difficult to test code in a controlled environment without mutating globals.

Mocking process globally can be brittle and may leak between tests. There are also related issues caused by process-dependent behavior (e.g. #809).

This seems related to the broader effort to reduce dependency on Node process (#953), but currently there is no way to provide a custom implementation.

Proposal

Allow passing a custom process-like object to memfs, for example when creating an fs instance:

const { fs } = memfs(json, {
  process: {
    cwd: () => "/app",
    platform: "win32",
    getuid: () => 1000,
  },
});

or:

const fs = createFsFromVolume(vol, {
  process: fakeProcess,
});

Benefits

Notes

This does not need to expose the full process API—only the parts memfs actually uses (e.g. cwd, platform, getuid) would be sufficient.

Happy to help with a PR if this direction makes sense 👍</issue_description>

Comments on the Issue (you are @copilot in this section)

@streamich > Happy to help with a PR if this direction makes sense 👍

Yes, this makes a lot of sense!</comment_new>


⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

Copilot AI changed the title [WIP] Add support for custom process implementation in memfs feat: allow injecting custom process implementation for better testability Mar 21, 2026
Copilot AI requested a review from streamich March 21, 2026 15:40
@streamich streamich marked this pull request as ready for review March 21, 2026 15:52
Copilot AI review requested due to automatic review settings March 21, 2026 15:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for injecting a custom process-like implementation into memfs/Volume/Superblock so tests can control cwd, platform, and uid/gid behavior without mutating global Node state.

Changes:

  • Extend memfs() to accept string | MemfsOptions with optional process injection and clarified cwd precedence.
  • Thread optional { process?: IProcess } through Volume.fromJSON / Volume.fromNestedJSON into Superblock.
  • Add tests validating injected process.cwd(), platform, and getuid/getgid behaviors; export IProcess from fs-core and re-export from memfs.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/memfs/src/index.ts Adds MemfsOptions and allows injecting process into volumes created by memfs(); re-exports IProcess.
packages/memfs/src/tests/memfs.process.test.ts New tests for memfs() cwd precedence and uid/gid seeding via injected process.
packages/fs-node/src/volume.ts Extends Volume.fromJSON / fromNestedJSON to accept { process?: IProcess } and pass it into Superblock.
packages/fs-node/src/tests/volume.process.test.ts New tests for Volume factory methods honoring injected process cwd/uid/gid.
packages/fs-core/src/Superblock.ts Stores injected process on the instance and uses it for cwd defaults, uid/gid seeding, and win32-specific error selection.
packages/fs-core/src/process.ts Refines IProcess.env typing to `Record<string, string
packages/fs-core/src/Node.ts Adds uid/gid parameters to the constructor so Superblock.createNode() can seed ownership.
packages/fs-core/src/index.ts Exports IProcess type from fs-core public API.
packages/fs-core/src/tests/Superblock.process.test.ts New tests for Superblock factories/constructor honoring injected process.

Comment on lines +52 to +56
constructor(ino: number, mode: number = 0o666, uid: number = getuid(), gid: number = getgid()) {
this.mode = mode;
this.ino = ino;
this._uid = uid;
this._gid = gid;
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Node still initializes _uid/_gid via field initializers (getuid()/getgid()) before the constructor body runs. With the new constructor parameters, this causes redundant UID/GID lookups and also still touches the default/global process even when Superblock.createNode() passes an injected uid/gid. Consider removing the field initializers and deriving defaults entirely inside the constructor (only when uid/gid args are not provided).

Copilot uses AI. Check for mistakes.
Comment on lines +214 to 221
const uid = this.process.getuid?.() ?? 0;
const gid = this.process.getgid?.() ?? 0;
while (i < steps.length) {
let node: Node = curr.getNode();
// Check access permissions if current link is a directory
if (node.isDirectory()) {
if (checkAccess && !node.canExecute()) {
if (checkAccess && !node.canExecute(uid, gid)) {
return Err(createStatError(ERROR_CODE.EACCES, funcName, filename));
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Access checks are now using the injected process uid/gid in walk() via node.canExecute(uid, gid), but other permission checks in Superblock (e.g. openLink and various write/mkdir/rm paths) still call node.canRead()/canWrite()/canExecute() without passing uid/gid, which falls back to the global/default process. This makes permission behavior inconsistent when a custom process is injected. Consider centralizing uid/gid retrieval from this.process and consistently passing them into all can* calls.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
/** Options for creating a memfs instance. */
export interface MemfsOptions {
/** Custom working directory for resolving relative paths. Defaults to `'/'`. */
cwd?: string;
/** Custom `process`-like object for controlling platform, uid, gid, and cwd behavior. */
process?: IProcess;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The MemfsOptions.cwd docstring says it defaults to '/', but when process is provided and cwd is omitted, the effective default becomes process.cwd() (per the logic below and the PR description). Update the comment to reflect the actual precedence to avoid misleading API consumers.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +99
export const memfs = (
json: NestedDirectoryJSON = {},
cwdOrOpts: string | MemfsOptions = '/',
): { fs: IFs; vol: Volume } => {
const opts: MemfsOptions = typeof cwdOrOpts === 'string' ? { cwd: cwdOrOpts } : cwdOrOpts;
// When no explicit cwd is given but a custom process is provided, let the
// Superblock use that process's cwd(). Otherwise default to '/' so the
// convenience function keeps its opinionated virtual-root default.
const cwd = opts.cwd ?? (opts.process ? undefined : '/');
const vol = Volume.fromNestedJSON(json, cwd, { process: opts.process });
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

PR description mentions createFsFromVolume(vol, { process: fakeProcess }), but createFsFromVolume still only accepts a single Volume argument and there is no way to pass a custom process at that stage. Either update the PR description/examples to match the implemented API, or extend createFsFromVolume to accept and apply such options.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

@streamich streamich left a comment

Choose a reason for hiding this comment

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

LGTM

@streamich streamich merged commit 2397c7d into master Mar 21, 2026
13 checks passed
@streamich streamich deleted the copilot/allow-injecting-custom-process-implementation branch March 21, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow injecting custom process implementation for better testability

3 participants