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 });