import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { mkdirSafe, existsSafe } from '../src/fs-utils'; describe('fs-utils.ts', () => { let tempDir: string; beforeEach(async () => { // Create a temporary directory for testing tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-utils-test-')); }); afterEach(async () => { // Clean up temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('mkdirSafe', () => { it('should create a new directory', async () => { const newDir = path.join(tempDir, 'new-directory'); await mkdirSafe(newDir); const stats = await fs.stat(newDir); expect(stats.isDirectory()).toBe(true); }); it('should create nested directories recursively', async () => { const nestedDir = path.join(tempDir, 'level1', 'level2', 'level3'); await mkdirSafe(nestedDir); const stats = await fs.stat(nestedDir); expect(stats.isDirectory()).toBe(true); }); it('should succeed when directory already exists', async () => { const existingDir = path.join(tempDir, 'existing'); await fs.mkdir(existingDir); await expect(mkdirSafe(existingDir)).resolves.not.toThrow(); }); it('should succeed when path is a symlink to a directory', async () => { const targetDir = path.join(tempDir, 'target'); const symlinkPath = path.join(tempDir, 'symlink'); await fs.mkdir(targetDir); await fs.symlink(targetDir, symlinkPath, 'dir'); await expect(mkdirSafe(symlinkPath)).resolves.not.toThrow(); }); it('should throw when path exists as a file', async () => { const filePath = path.join(tempDir, 'existing-file.txt'); await fs.writeFile(filePath, 'content'); await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory'); }); it('should resolve relative paths', async () => { const originalCwd = process.cwd(); try { process.chdir(tempDir); await mkdirSafe('relative-dir'); const stats = await fs.stat(path.join(tempDir, 'relative-dir')); expect(stats.isDirectory()).toBe(true); } finally { process.chdir(originalCwd); } }); it('should handle concurrent creation gracefully', async () => { const newDir = path.join(tempDir, 'concurrent'); const promises = [mkdirSafe(newDir), mkdirSafe(newDir), mkdirSafe(newDir)]; await expect(Promise.all(promises)).resolves.not.toThrow(); const stats = await fs.stat(newDir); expect(stats.isDirectory()).toBe(true); }); it('should handle paths with special characters', async () => { const specialDir = path.join(tempDir, 'dir with spaces & special-chars'); await mkdirSafe(specialDir); const stats = await fs.stat(specialDir); expect(stats.isDirectory()).toBe(true); }); }); describe('existsSafe', () => { it('should return true for existing directory', async () => { const existingDir = path.join(tempDir, 'exists'); await fs.mkdir(existingDir); const result = await existsSafe(existingDir); expect(result).toBe(true); }); it('should return true for existing file', async () => { const filePath = path.join(tempDir, 'file.txt'); await fs.writeFile(filePath, 'content'); const result = await existsSafe(filePath); expect(result).toBe(true); }); it('should return false for non-existent path', async () => { const nonExistent = path.join(tempDir, 'does-not-exist'); const result = await existsSafe(nonExistent); expect(result).toBe(false); }); it('should return true for symlink', async () => { const target = path.join(tempDir, 'target.txt'); const symlink = path.join(tempDir, 'link.txt'); await fs.writeFile(target, 'content'); await fs.symlink(target, symlink); const result = await existsSafe(symlink); expect(result).toBe(true); }); it('should return true for broken symlink', async () => { const symlink = path.join(tempDir, 'broken-link'); // Create symlink to non-existent target await fs.symlink('/non/existent/path', symlink); const result = await existsSafe(symlink); // lstat succeeds on broken symlinks expect(result).toBe(true); }); it('should handle relative paths', async () => { const originalCwd = process.cwd(); try { process.chdir(tempDir); await fs.writeFile('test.txt', 'content'); const result = await existsSafe('test.txt'); expect(result).toBe(true); } finally { process.chdir(originalCwd); } }); it('should handle paths with special characters', async () => { const specialFile = path.join(tempDir, 'file with spaces & chars.txt'); await fs.writeFile(specialFile, 'content'); const result = await existsSafe(specialFile); expect(result).toBe(true); }); it('should return false for parent of non-existent nested path', async () => { const nonExistent = path.join(tempDir, 'does', 'not', 'exist'); const result = await existsSafe(nonExistent); expect(result).toBe(false); }); }); describe('Error handling', () => { it('should handle permission errors in mkdirSafe', async () => { // Skip on Windows where permissions work differently if (process.platform === 'win32') { return; } const restrictedDir = path.join(tempDir, 'restricted'); await fs.mkdir(restrictedDir); // Make directory read-only await fs.chmod(restrictedDir, 0o444); const newDir = path.join(restrictedDir, 'new'); try { await expect(mkdirSafe(newDir)).rejects.toThrow(); } finally { // Restore permissions for cleanup await fs.chmod(restrictedDir, 0o755); } }); it('should propagate unexpected errors in existsSafe', async () => { const mockError = new Error('Unexpected error'); (mockError as any).code = 'EACCES'; const spy = vi.spyOn(fs, 'lstat').mockRejectedValueOnce(mockError); await expect(existsSafe('/some/path')).rejects.toThrow('Unexpected error'); spy.mockRestore(); }); }); describe('Integration scenarios', () => { it('should work together: check existence then create if missing', async () => { const dirPath = path.join(tempDir, 'check-then-create'); const existsBefore = await existsSafe(dirPath); expect(existsBefore).toBe(false); await mkdirSafe(dirPath); const existsAfter = await existsSafe(dirPath); expect(existsAfter).toBe(true); }); it('should handle nested directory creation with existence checks', async () => { const level1 = path.join(tempDir, 'level1'); const level2 = path.join(level1, 'level2'); const level3 = path.join(level2, 'level3'); await mkdirSafe(level3); expect(await existsSafe(level1)).toBe(true); expect(await existsSafe(level2)).toBe(true); expect(await existsSafe(level3)).toBe(true); }); }); });