/ src / tests / make-directory.test.ts
make-directory.test.ts
  1  import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
  2  import * as fs from "fs/promises";
  3  import * as path from "path";
  4  import * as os from "os";
  5  import { handleFileSystemTool } from "../tools/filesystem-tools.js";
  6  import { setAllowedDirectories } from "../utils/lib.js";
  7  
  8  describe("make_directory tool", () => {
  9    let testDir: string;
 10  
 11    beforeEach(async () => {
 12      testDir = await fs.mkdtemp(path.join(os.tmpdir(), "make-dir-test-"));
 13      setAllowedDirectories([testDir]);
 14    });
 15  
 16    afterEach(async () => {
 17      await fs.rm(testDir, { recursive: true, force: true });
 18    });
 19  
 20    describe("Single directory creation", () => {
 21      it("should create a single directory", async () => {
 22        const dirPath = path.join(testDir, "new-folder");
 23        const result = await handleFileSystemTool("make_directory", {
 24          paths: dirPath,
 25        });
 26  
 27        expect(result.content[0].text).toContain(
 28          "Successfully created directory"
 29        );
 30        await expect(fs.access(dirPath)).resolves.toBeUndefined();
 31      });
 32  
 33      it("should create nested directory structure", async () => {
 34        const dirPath = path.join(testDir, "level1", "level2", "level3");
 35        await handleFileSystemTool("make_directory", {
 36          paths: dirPath,
 37        });
 38  
 39        await expect(fs.access(dirPath)).resolves.toBeUndefined();
 40      });
 41  
 42      it("should be idempotent (existing directory doesn't error)", async () => {
 43        const dirPath = path.join(testDir, "existing");
 44        await fs.mkdir(dirPath);
 45  
 46        const result = await handleFileSystemTool("make_directory", {
 47          paths: dirPath,
 48        });
 49  
 50        expect(result.content[0].text).toContain("Successfully created");
 51      });
 52  
 53      it("should work with deeply nested paths", async () => {
 54        const dirPath = path.join(
 55          testDir,
 56          "very",
 57          "deeply",
 58          "nested",
 59          "directory",
 60          "structure"
 61        );
 62        await handleFileSystemTool("make_directory", {
 63          paths: dirPath,
 64        });
 65  
 66        await expect(fs.access(dirPath)).resolves.toBeUndefined();
 67      });
 68    });
 69  
 70    describe("Batch directory creation", () => {
 71      it("should create multiple directories concurrently", async () => {
 72        const paths = [
 73          path.join(testDir, "dir1"),
 74          path.join(testDir, "dir2"),
 75          path.join(testDir, "dir3"),
 76        ];
 77  
 78        const result = await handleFileSystemTool("make_directory", {
 79          paths: paths,
 80        });
 81  
 82        expect(result.content[0].text).toContain(
 83          "Successfully created 3 directories"
 84        );
 85  
 86        for (const p of paths) {
 87          await expect(fs.access(p)).resolves.toBeUndefined();
 88        }
 89      });
 90  
 91      it("should create nested structures in batch", async () => {
 92        const paths = [
 93          path.join(testDir, "project", "src", "components"),
 94          path.join(testDir, "project", "dist", "assets"),
 95          path.join(testDir, "project", "tests", "unit"),
 96        ];
 97  
 98        await handleFileSystemTool("make_directory", {
 99          paths: paths,
100        });
101  
102        for (const p of paths) {
103          await expect(fs.access(p)).resolves.toBeUndefined();
104        }
105      });
106  
107      it("should list all created directories in output", async () => {
108        const paths = [path.join(testDir, "a"), path.join(testDir, "b")];
109  
110        const result = await handleFileSystemTool("make_directory", {
111          paths: paths,
112        });
113  
114        const text = result.content[0].text;
115        expect(text).toContain(paths[0]);
116        expect(text).toContain(paths[1]);
117      });
118  
119      it("should handle large batch of directories", async () => {
120        const paths = Array.from({ length: 10 }, (_, i) =>
121          path.join(testDir, `batch-dir-${i}`)
122        );
123  
124        const result = await handleFileSystemTool("make_directory", {
125          paths: paths,
126        });
127  
128        expect(result.content[0].text).toContain(
129          "Successfully created 10 directories"
130        );
131  
132        for (const p of paths) {
133          await expect(fs.access(p)).resolves.toBeUndefined();
134        }
135      });
136    });
137  
138    describe("Error handling", () => {
139      it("should throw on invalid path", async () => {
140        await expect(
141          handleFileSystemTool("make_directory", {
142            paths: "/invalid/path/outside/allowed",
143          })
144        ).rejects.toThrow();
145      });
146  
147      it("should fail entire batch if one path is invalid", async () => {
148        const validPath = path.join(testDir, "valid");
149        const paths = [validPath, "/invalid/path"];
150  
151        await expect(
152          handleFileSystemTool("make_directory", {
153            paths: paths,
154          })
155        ).rejects.toThrow();
156  
157        // Valid path should not be created if batch fails
158        await expect(fs.access(validPath)).rejects.toThrow();
159      });
160  
161      it("should throw on invalid arguments", async () => {
162        await expect(
163          handleFileSystemTool("make_directory", {
164            paths: 123, // Invalid type
165          })
166        ).rejects.toThrow();
167      });
168  
169      it("should handle empty array gracefully", async () => {
170        const result = await handleFileSystemTool("make_directory", {
171          paths: [],
172        });
173  
174        expect(result.content[0].text).toContain("Successfully created 0");
175      });
176  
177      it("should reject paths with null bytes", async () => {
178        await expect(
179          handleFileSystemTool("make_directory", {
180            paths: path.join(testDir, "bad\x00path"),
181          })
182        ).rejects.toThrow();
183      });
184  
185      it("should block prefix collision attacks (CVE-2025-54794 pattern)", async () => {
186        // This test verifies that paths with similar prefixes but different directories
187        // are correctly rejected, preventing the path restriction bypass vulnerability
188        const baseDirName = path.basename(testDir);
189        const parentDir = path.dirname(testDir);
190        
191        // Create a directory with prefix matching the allowed directory name
192        // but is actually a sibling, not a subdirectory
193        const evilDir = path.join(parentDir, `${baseDirName}_evil`);
194        
195        // Ensure the evil directory exists (simulating attacker-controlled environment)
196        try {
197          await fs.mkdir(evilDir, { recursive: true });
198        } catch {
199          // Directory might already exist, continue
200        }
201        
202        // Attempt to create directory in the evil path - should be blocked
203        const attackPath = path.join(evilDir, "unauthorized");
204        await expect(
205          handleFileSystemTool("make_directory", {
206            paths: attackPath,
207          })
208        ).rejects.toThrow("Access denied");
209        
210        // Verify the directory was NOT created
211        await expect(fs.access(attackPath)).rejects.toThrow();
212        
213        // Cleanup
214        try {
215          await fs.rm(evilDir, { recursive: true, force: true });
216        } catch {
217          // Ignore cleanup errors
218        }
219      });
220  
221      it("should allow legitimate subdirectories despite prefix similarity", async () => {
222        // Verify that legitimate subdirectories still work correctly
223        const legitPath = path.join(testDir, "project", "src");
224        const result = await handleFileSystemTool("make_directory", {
225          paths: legitPath,
226        });
227  
228        expect(result.content[0].text).toContain("Successfully created directory");
229        await expect(fs.access(legitPath)).resolves.toBeUndefined();
230      });
231    });
232  
233    describe("Backward compatibility", () => {
234      it("should work with single string path", async () => {
235        const dirPath = path.join(testDir, "compat-test");
236  
237        const result = await handleFileSystemTool("make_directory", {
238          paths: dirPath,
239        });
240  
241        expect(result.content[0].text).toContain(
242          "Successfully created directory"
243        );
244        await expect(fs.access(dirPath)).resolves.toBeUndefined();
245      });
246  
247      it("should format single path output correctly", async () => {
248        const dirPath = path.join(testDir, "single");
249  
250        const result = await handleFileSystemTool("make_directory", {
251          paths: dirPath,
252        });
253  
254        const text = result.content[0].text;
255        expect(text).toContain("Successfully created directory");
256        expect(text).not.toContain("directories:");
257      });
258    });
259  
260    describe("Mixed scenarios", () => {
261      it("should handle batch with some existing directories", async () => {
262        const existingDir = path.join(testDir, "existing");
263        const newDir = path.join(testDir, "new");
264  
265        await fs.mkdir(existingDir);
266  
267        const result = await handleFileSystemTool("make_directory", {
268          paths: [existingDir, newDir],
269        });
270  
271        expect(result.content[0].text).toContain(
272          "Successfully created 2 directories"
273        );
274        await expect(fs.access(existingDir)).resolves.toBeUndefined();
275        await expect(fs.access(newDir)).resolves.toBeUndefined();
276      });
277  
278      it("should create sibling and nested directories together", async () => {
279        const paths = [
280          path.join(testDir, "sibling1"),
281          path.join(testDir, "sibling2"),
282          path.join(testDir, "nested", "deep", "structure"),
283        ];
284  
285        await handleFileSystemTool("make_directory", {
286          paths: paths,
287        });
288  
289        for (const p of paths) {
290          await expect(fs.access(p)).resolves.toBeUndefined();
291        }
292      });
293    });
294  
295    describe("MCP client serialization workaround", () => {
296      it("should handle stringified array from buggy MCP clients", async () => {
297        const paths = [
298          path.join(testDir, "stringified1"),
299          path.join(testDir, "stringified2"),
300          path.join(testDir, "stringified3"),
301        ];
302  
303        // Simulate buggy MCP client behavior - array is stringified
304        const stringifiedPaths = JSON.stringify(paths);
305        const consoleErrorSpy = jest
306          .spyOn(console, "error")
307          .mockImplementation(() => {});
308  
309        try {
310          const result = await handleFileSystemTool("make_directory", {
311            paths: stringifiedPaths, // Passing stringified array instead of actual array
312          });
313  
314          expect(result.content[0].text).toContain(
315            "Successfully created 3 directories",
316          );
317          expect(consoleErrorSpy).toHaveBeenCalledWith(
318            "[INFO] make_directory: Detected and corrected stringified array parameter",
319          );
320  
321          for (const p of paths) {
322            await expect(fs.access(p)).resolves.toBeUndefined();
323          }
324        } finally {
325          consoleErrorSpy.mockRestore();
326        }
327      });
328  
329      it("should still work with correctly formatted arrays", async () => {
330        const paths = [
331          path.join(testDir, "correct1"),
332          path.join(testDir, "correct2"),
333        ];
334  
335        // Proper array format (how it should be sent)
336        const result = await handleFileSystemTool("make_directory", {
337          paths: paths, // Proper array
338        });
339  
340        expect(result.content[0].text).toContain("Successfully created 2 directories");
341  
342        for (const p of paths) {
343          await expect(fs.access(p)).resolves.toBeUndefined();
344        }
345      });
346  
347      it("should handle paths that literally start with '[' character", async () => {
348        // Edge case: path that looks like it could be an array but isn't valid JSON
349        const weirdPath = path.join(testDir, "[bracket-folder]");
350  
351        const result = await handleFileSystemTool("make_directory", {
352          paths: weirdPath,
353        });
354  
355        expect(result.content[0].text).toContain("Successfully created directory");
356        await expect(fs.access(weirdPath)).resolves.toBeUndefined();
357      });
358  
359      it("should handle malformed stringified arrays gracefully", async () => {
360        // Malformed JSON that starts with '[' but isn't valid
361        const malformedPath = path.join(testDir, "[not-valid-json");
362  
363        const result = await handleFileSystemTool("make_directory", {
364          paths: malformedPath,
365        });
366  
367        // Should treat as single path since JSON.parse fails
368        expect(result.content[0].text).toContain("Successfully created directory");
369        await expect(fs.access(malformedPath)).resolves.toBeUndefined();
370      });
371    });
372  });