@ricsam/isolate-fs 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +30 -7
- package/src/fixtures/test-image.png +0 -0
- package/src/index.test.ts +882 -0
- package/src/index.ts +997 -0
- package/src/integration.test.ts +288 -0
- package/src/node-adapter.test.ts +337 -0
- package/src/node-adapter.ts +300 -0
- package/src/streaming.test.ts +634 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, test, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createRuntime, type RuntimeHandle } from "@ricsam/isolate-runtime";
|
|
4
|
+
import { createNodeFileSystemHandler } from "./node-adapter.ts";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const fixturesDir = join(__dirname, "fixtures");
|
|
12
|
+
|
|
13
|
+
describe("FS + HTTP Integration Tests", () => {
|
|
14
|
+
let runtime: RuntimeHandle | undefined;
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
if (runtime) {
|
|
18
|
+
runtime.dispose();
|
|
19
|
+
runtime = undefined;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("binary file (image) can be read from disk and returned in response", async () => {
|
|
24
|
+
// 1. Read original image bytes for comparison
|
|
25
|
+
const originalBytes = fs.readFileSync(join(fixturesDir, "test-image.png"));
|
|
26
|
+
|
|
27
|
+
// 2. Create runtime with fs enabled, pointing to fixtures dir
|
|
28
|
+
runtime = await createRuntime({
|
|
29
|
+
console: {
|
|
30
|
+
onLog: () => {}, // Suppress logs
|
|
31
|
+
},
|
|
32
|
+
fs: {
|
|
33
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 3. Run code that reads file and returns as Response
|
|
38
|
+
const result = await runtime.context.eval(
|
|
39
|
+
`
|
|
40
|
+
(async () => {
|
|
41
|
+
const root = await getDirectory("/");
|
|
42
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
43
|
+
const file = await fileHandle.getFile();
|
|
44
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
45
|
+
|
|
46
|
+
const response = new Response(arrayBuffer, {
|
|
47
|
+
headers: { "Content-Type": "image/png" }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Return the response body as array for comparison
|
|
51
|
+
const responseBuffer = await response.arrayBuffer();
|
|
52
|
+
return JSON.stringify(Array.from(new Uint8Array(responseBuffer)));
|
|
53
|
+
})()
|
|
54
|
+
`,
|
|
55
|
+
{ promise: true }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// 4. Verify response matches original file
|
|
59
|
+
const responseBytes = new Uint8Array(JSON.parse(result as string));
|
|
60
|
+
assert.deepStrictEqual(responseBytes, new Uint8Array(originalBytes));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("new Response(file) streams file directly (WHATWG compliant)", async () => {
|
|
64
|
+
// 1. Read original image bytes for comparison
|
|
65
|
+
const originalBytes = fs.readFileSync(join(fixturesDir, "test-image.png"));
|
|
66
|
+
|
|
67
|
+
// 2. Create runtime with fs enabled
|
|
68
|
+
runtime = await createRuntime({
|
|
69
|
+
console: {
|
|
70
|
+
onLog: () => {},
|
|
71
|
+
},
|
|
72
|
+
fs: {
|
|
73
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 3. Run code that passes File directly to Response
|
|
78
|
+
const result = await runtime.context.eval(
|
|
79
|
+
`
|
|
80
|
+
(async () => {
|
|
81
|
+
const root = await getDirectory("/");
|
|
82
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
83
|
+
const file = await fileHandle.getFile();
|
|
84
|
+
|
|
85
|
+
// Pass File directly to Response - should stream automatically
|
|
86
|
+
const response = new Response(file);
|
|
87
|
+
|
|
88
|
+
// Return the response body as array for comparison
|
|
89
|
+
const responseBuffer = await response.arrayBuffer();
|
|
90
|
+
return JSON.stringify(Array.from(new Uint8Array(responseBuffer)));
|
|
91
|
+
})()
|
|
92
|
+
`,
|
|
93
|
+
{ promise: true }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 4. Verify response matches original file
|
|
97
|
+
const responseBytes = new Uint8Array(JSON.parse(result as string));
|
|
98
|
+
assert.deepStrictEqual(responseBytes, new Uint8Array(originalBytes));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("new Response(blob) streams blob directly (WHATWG compliant)", async () => {
|
|
102
|
+
// 1. Read original image bytes for comparison
|
|
103
|
+
const originalBytes = fs.readFileSync(join(fixturesDir, "test-image.png"));
|
|
104
|
+
|
|
105
|
+
// 2. Create runtime with fs enabled
|
|
106
|
+
runtime = await createRuntime({
|
|
107
|
+
console: {
|
|
108
|
+
onLog: () => {},
|
|
109
|
+
},
|
|
110
|
+
fs: {
|
|
111
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 3. Run code that creates Blob from file and passes to Response
|
|
116
|
+
const result = await runtime.context.eval(
|
|
117
|
+
`
|
|
118
|
+
(async () => {
|
|
119
|
+
const root = await getDirectory("/");
|
|
120
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
121
|
+
const file = await fileHandle.getFile();
|
|
122
|
+
|
|
123
|
+
// Create a Blob from file contents and pass directly to Response
|
|
124
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
125
|
+
const blob = new Blob([arrayBuffer], { type: "image/png" });
|
|
126
|
+
const response = new Response(blob);
|
|
127
|
+
|
|
128
|
+
// Return the response body as array for comparison
|
|
129
|
+
const responseBuffer = await response.arrayBuffer();
|
|
130
|
+
return JSON.stringify(Array.from(new Uint8Array(responseBuffer)));
|
|
131
|
+
})()
|
|
132
|
+
`,
|
|
133
|
+
{ promise: true }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// 4. Verify response matches original file
|
|
137
|
+
const responseBytes = new Uint8Array(JSON.parse(result as string));
|
|
138
|
+
assert.deepStrictEqual(responseBytes, new Uint8Array(originalBytes));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("new Response(file.slice()) returns sliced blob (WHATWG compliant)", async () => {
|
|
142
|
+
// 1. Read original image bytes for comparison
|
|
143
|
+
const originalBytes = fs.readFileSync(join(fixturesDir, "test-image.png"));
|
|
144
|
+
|
|
145
|
+
// 2. Create runtime with fs enabled
|
|
146
|
+
runtime = await createRuntime({
|
|
147
|
+
console: {
|
|
148
|
+
onLog: () => {},
|
|
149
|
+
},
|
|
150
|
+
fs: {
|
|
151
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 3. Run code that uses file.slice() - File extends Blob
|
|
156
|
+
const result = await runtime.context.eval(
|
|
157
|
+
`
|
|
158
|
+
(async () => {
|
|
159
|
+
const root = await getDirectory("/");
|
|
160
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
161
|
+
const file = await fileHandle.getFile();
|
|
162
|
+
|
|
163
|
+
// Use file.slice() to get first 100 bytes - File extends Blob
|
|
164
|
+
const slicedBlob = file.slice(0, 100);
|
|
165
|
+
const response = new Response(slicedBlob);
|
|
166
|
+
|
|
167
|
+
// Return the response body as array for comparison
|
|
168
|
+
const responseBuffer = await response.arrayBuffer();
|
|
169
|
+
return JSON.stringify(Array.from(new Uint8Array(responseBuffer)));
|
|
170
|
+
})()
|
|
171
|
+
`,
|
|
172
|
+
{ promise: true }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// 4. Verify response is first 100 bytes
|
|
176
|
+
const responseBytes = new Uint8Array(JSON.parse(result as string));
|
|
177
|
+
const expectedSlice = new Uint8Array(originalBytes).slice(0, 100);
|
|
178
|
+
assert.deepStrictEqual(responseBytes, expectedSlice);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("file.type returns correct MIME type", async () => {
|
|
182
|
+
runtime = await createRuntime({
|
|
183
|
+
console: {
|
|
184
|
+
onLog: () => {},
|
|
185
|
+
},
|
|
186
|
+
fs: {
|
|
187
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await runtime.context.eval(
|
|
192
|
+
`
|
|
193
|
+
(async () => {
|
|
194
|
+
const root = await getDirectory("/");
|
|
195
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
196
|
+
const file = await fileHandle.getFile();
|
|
197
|
+
return file.type;
|
|
198
|
+
})()
|
|
199
|
+
`,
|
|
200
|
+
{ promise: true }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
assert.strictEqual(result, "image/png");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("file.name returns correct filename", async () => {
|
|
207
|
+
runtime = await createRuntime({
|
|
208
|
+
console: {
|
|
209
|
+
onLog: () => {},
|
|
210
|
+
},
|
|
211
|
+
fs: {
|
|
212
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await runtime.context.eval(
|
|
217
|
+
`
|
|
218
|
+
(async () => {
|
|
219
|
+
const root = await getDirectory("/");
|
|
220
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
221
|
+
const file = await fileHandle.getFile();
|
|
222
|
+
return file.name;
|
|
223
|
+
})()
|
|
224
|
+
`,
|
|
225
|
+
{ promise: true }
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
assert.strictEqual(result, "test-image.png");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("file.size returns correct size", async () => {
|
|
232
|
+
const originalBytes = fs.readFileSync(join(fixturesDir, "test-image.png"));
|
|
233
|
+
|
|
234
|
+
runtime = await createRuntime({
|
|
235
|
+
console: {
|
|
236
|
+
onLog: () => {},
|
|
237
|
+
},
|
|
238
|
+
fs: {
|
|
239
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await runtime.context.eval(
|
|
244
|
+
`
|
|
245
|
+
(async () => {
|
|
246
|
+
const root = await getDirectory("/");
|
|
247
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
248
|
+
const file = await fileHandle.getFile();
|
|
249
|
+
return file.size;
|
|
250
|
+
})()
|
|
251
|
+
`,
|
|
252
|
+
{ promise: true }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
assert.strictEqual(result, originalBytes.length);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("file instanceof checks work correctly", async () => {
|
|
259
|
+
runtime = await createRuntime({
|
|
260
|
+
console: {
|
|
261
|
+
onLog: () => {},
|
|
262
|
+
},
|
|
263
|
+
fs: {
|
|
264
|
+
getDirectory: async () => createNodeFileSystemHandler(fixturesDir),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await runtime.context.eval(
|
|
269
|
+
`
|
|
270
|
+
(async () => {
|
|
271
|
+
const root = await getDirectory("/");
|
|
272
|
+
const fileHandle = await root.getFileHandle("test-image.png");
|
|
273
|
+
const file = await fileHandle.getFile();
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
isFile: file instanceof File,
|
|
276
|
+
isBlob: file instanceof Blob,
|
|
277
|
+
});
|
|
278
|
+
})()
|
|
279
|
+
`,
|
|
280
|
+
{ promise: true }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
assert.deepStrictEqual(JSON.parse(result as string), {
|
|
284
|
+
isFile: true,
|
|
285
|
+
isBlob: true,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { test, describe, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createFsFromVolume, Volume } from "memfs";
|
|
4
|
+
import { createNodeFileSystemHandler } from "./node-adapter.ts";
|
|
5
|
+
import type { FileSystemHandler } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
describe("createNodeFileSystemHandler", () => {
|
|
8
|
+
let vol: InstanceType<typeof Volume>;
|
|
9
|
+
let handler: FileSystemHandler;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vol = new Volume();
|
|
13
|
+
const memfs = createFsFromVolume(vol);
|
|
14
|
+
handler = createNodeFileSystemHandler("/", { fs: memfs as any });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("getFileHandle", () => {
|
|
18
|
+
test("creates new file when create: true", async () => {
|
|
19
|
+
await handler.getFileHandle("/newfile.txt", { create: true });
|
|
20
|
+
assert.ok(vol.existsSync("/newfile.txt"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("opens existing file without create option", async () => {
|
|
24
|
+
vol.writeFileSync("/existing.txt", "content");
|
|
25
|
+
await handler.getFileHandle("/existing.txt");
|
|
26
|
+
// Should not throw
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("throws NotFoundError for missing file without create", async () => {
|
|
30
|
+
await assert.rejects(
|
|
31
|
+
() => handler.getFileHandle("/nonexistent.txt"),
|
|
32
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("throws TypeMismatchError when path is a directory", async () => {
|
|
37
|
+
vol.mkdirSync("/somedir");
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
() => handler.getFileHandle("/somedir"),
|
|
40
|
+
(err: Error) => err.message.includes("[TypeMismatchError]")
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("getDirectoryHandle", () => {
|
|
46
|
+
test("creates new directory when create: true", async () => {
|
|
47
|
+
await handler.getDirectoryHandle("/newdir", { create: true });
|
|
48
|
+
assert.ok(vol.existsSync("/newdir"));
|
|
49
|
+
const stats = vol.statSync("/newdir");
|
|
50
|
+
assert.ok(stats.isDirectory());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("creates nested directories with create: true", async () => {
|
|
54
|
+
await handler.getDirectoryHandle("/parent/child/grandchild", { create: true });
|
|
55
|
+
assert.ok(vol.existsSync("/parent/child/grandchild"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("opens existing directory without create option", async () => {
|
|
59
|
+
vol.mkdirSync("/existingdir");
|
|
60
|
+
await handler.getDirectoryHandle("/existingdir");
|
|
61
|
+
// Should not throw
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("throws NotFoundError for missing directory without create", async () => {
|
|
65
|
+
await assert.rejects(
|
|
66
|
+
() => handler.getDirectoryHandle("/nonexistent"),
|
|
67
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("throws TypeMismatchError when path is a file", async () => {
|
|
72
|
+
vol.writeFileSync("/somefile", "content");
|
|
73
|
+
await assert.rejects(
|
|
74
|
+
() => handler.getDirectoryHandle("/somefile"),
|
|
75
|
+
(err: Error) => err.message.includes("[TypeMismatchError]")
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("removeEntry", () => {
|
|
81
|
+
test("removes a file", async () => {
|
|
82
|
+
vol.writeFileSync("/todelete.txt", "content");
|
|
83
|
+
await handler.removeEntry("/todelete.txt");
|
|
84
|
+
assert.ok(!vol.existsSync("/todelete.txt"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("removes an empty directory", async () => {
|
|
88
|
+
vol.mkdirSync("/emptydir");
|
|
89
|
+
await handler.removeEntry("/emptydir");
|
|
90
|
+
assert.ok(!vol.existsSync("/emptydir"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("removes non-empty directory with recursive: true", async () => {
|
|
94
|
+
vol.mkdirSync("/parent");
|
|
95
|
+
vol.mkdirSync("/parent/child");
|
|
96
|
+
vol.writeFileSync("/parent/file.txt", "content");
|
|
97
|
+
vol.writeFileSync("/parent/child/nested.txt", "nested");
|
|
98
|
+
|
|
99
|
+
await handler.removeEntry("/parent", { recursive: true });
|
|
100
|
+
|
|
101
|
+
assert.ok(!vol.existsSync("/parent"));
|
|
102
|
+
assert.ok(!vol.existsSync("/parent/child"));
|
|
103
|
+
assert.ok(!vol.existsSync("/parent/file.txt"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("throws error for non-empty directory without recursive", async () => {
|
|
107
|
+
vol.mkdirSync("/nonempty");
|
|
108
|
+
vol.writeFileSync("/nonempty/file.txt", "content");
|
|
109
|
+
|
|
110
|
+
await assert.rejects(
|
|
111
|
+
() => handler.removeEntry("/nonempty"),
|
|
112
|
+
(err: Error) => err.message.includes("[InvalidModificationError]")
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("throws NotFoundError for non-existent path", async () => {
|
|
117
|
+
await assert.rejects(
|
|
118
|
+
() => handler.removeEntry("/nonexistent"),
|
|
119
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("readDirectory", () => {
|
|
125
|
+
test("lists files and directories", async () => {
|
|
126
|
+
vol.mkdirSync("/testdir");
|
|
127
|
+
vol.writeFileSync("/testdir/file1.txt", "content1");
|
|
128
|
+
vol.writeFileSync("/testdir/file2.txt", "content2");
|
|
129
|
+
vol.mkdirSync("/testdir/subdir");
|
|
130
|
+
|
|
131
|
+
const entries = await handler.readDirectory("/testdir");
|
|
132
|
+
|
|
133
|
+
assert.strictEqual(entries.length, 3);
|
|
134
|
+
|
|
135
|
+
const sorted = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
136
|
+
assert.deepStrictEqual(sorted[0], { name: "file1.txt", kind: "file" });
|
|
137
|
+
assert.deepStrictEqual(sorted[1], { name: "file2.txt", kind: "file" });
|
|
138
|
+
assert.deepStrictEqual(sorted[2], { name: "subdir", kind: "directory" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns empty array for empty directory", async () => {
|
|
142
|
+
vol.mkdirSync("/emptydir");
|
|
143
|
+
const entries = await handler.readDirectory("/emptydir");
|
|
144
|
+
assert.strictEqual(entries.length, 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("throws NotFoundError for non-existent directory", async () => {
|
|
148
|
+
await assert.rejects(
|
|
149
|
+
() => handler.readDirectory("/nonexistent"),
|
|
150
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("readFile", () => {
|
|
156
|
+
test("reads file content as Uint8Array", async () => {
|
|
157
|
+
const content = "hello world";
|
|
158
|
+
vol.writeFileSync("/test.txt", content);
|
|
159
|
+
|
|
160
|
+
const result = await handler.readFile("/test.txt");
|
|
161
|
+
|
|
162
|
+
assert.ok(result.data instanceof Uint8Array);
|
|
163
|
+
assert.strictEqual(new TextDecoder().decode(result.data), content);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("returns correct size", async () => {
|
|
167
|
+
const content = "hello world";
|
|
168
|
+
vol.writeFileSync("/test.txt", content);
|
|
169
|
+
|
|
170
|
+
const result = await handler.readFile("/test.txt");
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(result.size, content.length);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns lastModified timestamp", async () => {
|
|
176
|
+
vol.writeFileSync("/test.txt", "content");
|
|
177
|
+
|
|
178
|
+
const result = await handler.readFile("/test.txt");
|
|
179
|
+
|
|
180
|
+
assert.ok(typeof result.lastModified === "number");
|
|
181
|
+
assert.ok(result.lastModified > 0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns MIME type based on extension", async () => {
|
|
185
|
+
vol.writeFileSync("/test.txt", "content");
|
|
186
|
+
vol.writeFileSync("/test.json", "{}");
|
|
187
|
+
vol.writeFileSync("/test.png", "binary");
|
|
188
|
+
|
|
189
|
+
const txtResult = await handler.readFile("/test.txt");
|
|
190
|
+
const jsonResult = await handler.readFile("/test.json");
|
|
191
|
+
const pngResult = await handler.readFile("/test.png");
|
|
192
|
+
|
|
193
|
+
assert.strictEqual(txtResult.type, "text/plain");
|
|
194
|
+
assert.strictEqual(jsonResult.type, "application/json");
|
|
195
|
+
assert.strictEqual(pngResult.type, "image/png");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("throws NotFoundError for non-existent file", async () => {
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
() => handler.readFile("/nonexistent.txt"),
|
|
201
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("throws TypeMismatchError for directory", async () => {
|
|
206
|
+
vol.mkdirSync("/somedir");
|
|
207
|
+
await assert.rejects(
|
|
208
|
+
() => handler.readFile("/somedir"),
|
|
209
|
+
(err: Error) => err.message.includes("[TypeMismatchError]")
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("writeFile", () => {
|
|
215
|
+
test("writes data to file (overwrite mode)", async () => {
|
|
216
|
+
vol.writeFileSync("/test.txt", "old content");
|
|
217
|
+
|
|
218
|
+
const newContent = new TextEncoder().encode("new content");
|
|
219
|
+
await handler.writeFile("/test.txt", newContent);
|
|
220
|
+
|
|
221
|
+
const result = vol.readFileSync("/test.txt", "utf-8");
|
|
222
|
+
assert.strictEqual(result, "new content");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("writes data at specific position", async () => {
|
|
226
|
+
vol.writeFileSync("/test.txt", "hello world");
|
|
227
|
+
|
|
228
|
+
const data = new TextEncoder().encode("XXXXX");
|
|
229
|
+
await handler.writeFile("/test.txt", data, 6);
|
|
230
|
+
|
|
231
|
+
const result = vol.readFileSync("/test.txt", "utf-8");
|
|
232
|
+
assert.strictEqual(result, "hello XXXXX");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("throws NotFoundError for non-existent file", async () => {
|
|
236
|
+
const data = new TextEncoder().encode("content");
|
|
237
|
+
await assert.rejects(
|
|
238
|
+
() => handler.writeFile("/nonexistent.txt", data),
|
|
239
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("truncateFile", () => {
|
|
245
|
+
test("truncates file to smaller size", async () => {
|
|
246
|
+
vol.writeFileSync("/test.txt", "hello world");
|
|
247
|
+
|
|
248
|
+
await handler.truncateFile("/test.txt", 5);
|
|
249
|
+
|
|
250
|
+
const result = vol.readFileSync("/test.txt", "utf-8");
|
|
251
|
+
assert.strictEqual(result, "hello");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("extends file to larger size", async () => {
|
|
255
|
+
vol.writeFileSync("/test.txt", "hi");
|
|
256
|
+
|
|
257
|
+
await handler.truncateFile("/test.txt", 10);
|
|
258
|
+
|
|
259
|
+
const stats = vol.statSync("/test.txt");
|
|
260
|
+
assert.strictEqual(stats.size, 10);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("throws NotFoundError for non-existent file", async () => {
|
|
264
|
+
await assert.rejects(
|
|
265
|
+
() => handler.truncateFile("/nonexistent.txt", 10),
|
|
266
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("getFileMetadata", () => {
|
|
272
|
+
test("returns size", async () => {
|
|
273
|
+
const content = "hello world";
|
|
274
|
+
vol.writeFileSync("/test.txt", content);
|
|
275
|
+
|
|
276
|
+
const result = await handler.getFileMetadata("/test.txt");
|
|
277
|
+
|
|
278
|
+
assert.strictEqual(result.size, content.length);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("returns lastModified timestamp", async () => {
|
|
282
|
+
vol.writeFileSync("/test.txt", "content");
|
|
283
|
+
|
|
284
|
+
const result = await handler.getFileMetadata("/test.txt");
|
|
285
|
+
|
|
286
|
+
assert.ok(typeof result.lastModified === "number");
|
|
287
|
+
assert.ok(result.lastModified > 0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("returns MIME type based on extension", async () => {
|
|
291
|
+
vol.writeFileSync("/test.html", "<html></html>");
|
|
292
|
+
|
|
293
|
+
const result = await handler.getFileMetadata("/test.html");
|
|
294
|
+
|
|
295
|
+
assert.strictEqual(result.type, "text/html");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("throws NotFoundError for non-existent file", async () => {
|
|
299
|
+
await assert.rejects(
|
|
300
|
+
() => handler.getFileMetadata("/nonexistent.txt"),
|
|
301
|
+
(err: Error) => err.message.includes("[NotFoundError]")
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("throws TypeMismatchError for directory", async () => {
|
|
306
|
+
vol.mkdirSync("/somedir");
|
|
307
|
+
await assert.rejects(
|
|
308
|
+
() => handler.getFileMetadata("/somedir"),
|
|
309
|
+
(err: Error) => err.message.includes("[TypeMismatchError]")
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("path mapping", () => {
|
|
315
|
+
test("maps root path correctly", async () => {
|
|
316
|
+
const memfs = createFsFromVolume(vol);
|
|
317
|
+
const rootHandler = createNodeFileSystemHandler("/sandbox", { fs: memfs as any });
|
|
318
|
+
|
|
319
|
+
vol.mkdirSync("/sandbox", { recursive: true });
|
|
320
|
+
vol.writeFileSync("/sandbox/test.txt", "content");
|
|
321
|
+
|
|
322
|
+
const result = await rootHandler.readFile("/test.txt");
|
|
323
|
+
assert.strictEqual(new TextDecoder().decode(result.data), "content");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("handles nested paths correctly", async () => {
|
|
327
|
+
const memfs = createFsFromVolume(vol);
|
|
328
|
+
const rootHandler = createNodeFileSystemHandler("/sandbox", { fs: memfs as any });
|
|
329
|
+
|
|
330
|
+
vol.mkdirSync("/sandbox/subdir", { recursive: true });
|
|
331
|
+
vol.writeFileSync("/sandbox/subdir/file.txt", "nested content");
|
|
332
|
+
|
|
333
|
+
const result = await rootHandler.readFile("/subdir/file.txt");
|
|
334
|
+
assert.strictEqual(new TextDecoder().decode(result.data), "nested content");
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|