Conversation
…ility Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> Agent-Logs-Url: https://github.com/streamich/memfs/sessions/d6fc2bdc-cd95-483f-a59b-5fbc959a43f2
process implementation for better testability
There was a problem hiding this comment.
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 acceptstring | MemfsOptionswith optionalprocessinjection and clarified cwd precedence. - Thread optional
{ process?: IProcess }throughVolume.fromJSON/Volume.fromNestedJSONintoSuperblock. - Add tests validating injected
process.cwd(),platform, andgetuid/getgidbehaviors; exportIProcessfromfs-coreand re-export frommemfs.
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. |
| constructor(ino: number, mode: number = 0o666, uid: number = getuid(), gid: number = getgid()) { | ||
| this.mode = mode; | ||
| this.ino = ino; | ||
| this._uid = uid; | ||
| this._gid = gid; |
There was a problem hiding this comment.
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).
| 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)); |
There was a problem hiding this comment.
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.
| /** 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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 }); |
There was a problem hiding this comment.
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.
memfsreads directly from the global Nodeprocessobject (cwd(),platform,getuid()), making controlled testing without mutating globals impossible. This adds first-class support for injecting a customprocess-like object at the fs instance level.API changes
memfs()— second argument now acceptsstring | MemfsOptions(string form is backward-compatible):Volume.fromJSON/Volume.fromNestedJSON— accept optional{ process?: IProcess }third argument:Superblockconstructor — accepts{ process?: IProcess }:Internal changes
Superblock: storesthis.process(defaults to global singleton);createNode()reads uid/gid from it;walk()uses it for platform-specific error codes and access checks;fromJSON()usesthis.process.cwd()as the default cwd.Node: constructor accepts optionaluid/gidparams soSuperblock.createNode()can seed ownership from the injected process.IProcessis now exported from@jsonjoy.com/fs-coreand re-exported frommemfs.cwd resolution precedence in
memfs()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
processimplementation for better testability #1247⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.