1+ import { SystemError } from "@effect/platform/Error"
12import * as FileSystem from "@effect/platform/FileSystem"
23import * as Path from "@effect/platform/Path"
34import { describe , expect , it } from "@effect/vitest"
@@ -55,6 +56,63 @@ const memoryFileInfo = (entry: MemoryFileEntry): FileSystem.File.Info => ({
5556 uid : Option . none ( )
5657} )
5758
59+ /**
60+ * Builds a typed FileSystem error for the in-memory test filesystem.
61+ *
62+ * @param method - FileSystem method name that observed the invalid path.
63+ * @param requestedPath - Normalized memory path associated with the failure.
64+ * @param reason - Platform filesystem reason reported by the mock.
65+ * @param description - Human-readable failure description.
66+ * @returns Platform SystemError compatible with FileSystem effects.
67+ * @pure true
68+ * @effect none
69+ * @invariant The produced error is always scoped to the FileSystem module.
70+ * @precondition `method`, `requestedPath`, and `description` are finite strings.
71+ * @postcondition The returned error preserves the failing path in `pathOrDescriptor`.
72+ * @complexity O(1) time and space.
73+ * @throws Never
74+ */
75+ const memoryFileSystemError = (
76+ method : string ,
77+ requestedPath : string ,
78+ reason : "BadResource" | "NotFound" ,
79+ description : string
80+ ) : SystemError =>
81+ new SystemError ( {
82+ description,
83+ method,
84+ module : "FileSystem" ,
85+ pathOrDescriptor : requestedPath ,
86+ reason
87+ } )
88+
89+ /**
90+ * Looks up an in-memory file entry with real FileSystem missing-path semantics.
91+ *
92+ * @param entries - Current memory filesystem entries.
93+ * @param requestedPath - Path requested by the FileSystem operation.
94+ * @param method - FileSystem method name for typed error reporting.
95+ * @returns Effect that succeeds with the entry or fails when the path is absent.
96+ * @pure true
97+ * @effect Effect.fail or Effect.succeed
98+ * @invariant Missing paths are represented as typed NotFound failures.
99+ * @precondition `requestedPath` is a finite path string.
100+ * @postcondition Success implies the normalized path exists in `entries`.
101+ * @complexity O(p) time and O(p) space where p = |requestedPath|.
102+ * @throws Never
103+ */
104+ const requireMemoryEntry = (
105+ entries : ReadonlyMap < string , MemoryFileEntry > ,
106+ requestedPath : string ,
107+ method : string
108+ ) : Effect . Effect < MemoryFileEntry , SystemError > => {
109+ const normalized = normalizeMemoryPath ( requestedPath )
110+ const entry = entries . get ( normalized )
111+ return entry === undefined
112+ ? Effect . fail ( memoryFileSystemError ( method , normalized , "NotFound" , "Missing memory filesystem entry." ) )
113+ : Effect . succeed ( entry )
114+ }
115+
58116const createMemoryFileSystemLayer = ( ) => {
59117 let entries = new Map < string , MemoryFileEntry > ( [
60118 [ "/memory" , { _tag : "Directory" } ]
@@ -67,8 +125,16 @@ const createMemoryFileSystemLayer = () => {
67125 entries = new Map ( entries ) . set ( normalizeMemoryPath ( path ) , { _tag : "Directory" } )
68126 } ) ,
69127 readDirectory : ( path ) =>
70- Effect . sync ( ( ) => {
128+ Effect . gen ( function * ( _ ) {
71129 const directory = normalizeMemoryPath ( path )
130+ const entry = yield * _ ( requireMemoryEntry ( entries , directory , "readDirectory" ) )
131+ if ( entry . _tag !== "Directory" ) {
132+ return yield * _ (
133+ Effect . fail (
134+ memoryFileSystemError ( "readDirectory" , directory , "BadResource" , "Memory entry is not a directory." )
135+ )
136+ )
137+ }
72138 const prefix = directory === "/" ? "/" : `${ directory } /`
73139 const names = new Set < string > ( )
74140 for ( const candidate of entries . keys ( ) ) {
@@ -83,11 +149,18 @@ const createMemoryFileSystemLayer = () => {
83149 return [ ...names ]
84150 } ) ,
85151 readFileString : ( path ) =>
86- Effect . sync ( ( ) => {
87- const entry = entries . get ( normalizeMemoryPath ( path ) )
88- return entry ?. _tag === "File" ? entry . contents : ""
152+ Effect . gen ( function * ( _ ) {
153+ const normalized = normalizeMemoryPath ( path )
154+ const entry = yield * _ ( requireMemoryEntry ( entries , normalized , "readFileString" ) )
155+ return entry . _tag === "File"
156+ ? entry . contents
157+ : yield * _ (
158+ Effect . fail (
159+ memoryFileSystemError ( "readFileString" , normalized , "BadResource" , "Memory entry is not a file." )
160+ )
161+ )
89162 } ) ,
90- stat : ( path ) => Effect . sync ( ( ) => memoryFileInfo ( entries . get ( normalizeMemoryPath ( path ) ) ?? { _tag : "Directory" } ) ) ,
163+ stat : ( path ) => requireMemoryEntry ( entries , path , "stat" ) . pipe ( Effect . map ( ( entry ) => memoryFileInfo ( entry ) ) ) ,
91164 writeFileString : ( path , contents ) =>
92165 Effect . sync ( ( ) => {
93166 entries = new Map ( entries ) . set ( normalizeMemoryPath ( path ) , { _tag : "File" , contents } )
0 commit comments