import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mkdirSafe, existsSafe } from '@automaker/utils'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; describe('fs-utils.ts', () => { let testDir: string; beforeEach(async () => { // Create a temporary test directory testDir = path.join(os.tmpdir(), `fs-utils-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); }); afterEach(async () => { // Clean up test directory try { await fs.rm(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); describe('mkdirSafe', () => { it('should create a new directory', async () => { const newDir = path.join(testDir, 'new-directory'); await mkdirSafe(newDir); const stats = await fs.stat(newDir); expect(stats.isDirectory()).toBe(true); }); it('should succeed if directory already exists', async () => { const existingDir = path.join(testDir, 'existing'); await fs.mkdir(existingDir); // Should not throw await expect(mkdirSafe(existingDir)).resolves.toBeUndefined(); }); it('should create nested directories', async () => { const nestedDir = path.join(testDir, 'a', 'b', 'c'); await mkdirSafe(nestedDir); const stats = await fs.stat(nestedDir); expect(stats.isDirectory()).toBe(true); }); it('should throw if path exists as a file', async () => { const filePath = path.join(testDir, 'file.txt'); await fs.writeFile(filePath, 'content'); await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory'); }); it('should succeed if path is a symlink to a directory', async () => { const realDir = path.join(testDir, 'real-dir'); const symlinkPath = path.join(testDir, 'link-to-dir'); await fs.mkdir(realDir); await fs.symlink(realDir, symlinkPath); // Should not throw await expect(mkdirSafe(symlinkPath)).resolves.toBeUndefined(); }); it('should handle ELOOP error gracefully when checking path', async () => { // Mock lstat to throw ELOOP error const originalLstat = fs.lstat; const mkdirSafePath = path.join(testDir, 'eloop-path'); vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' }); // Should not throw, should return gracefully await expect(mkdirSafe(mkdirSafePath)).resolves.toBeUndefined(); vi.restoreAllMocks(); }); it('should handle EEXIST error gracefully when creating directory', async () => { const newDir = path.join(testDir, 'race-condition-dir'); // Mock lstat to return ENOENT (path doesn't exist) // Then mock mkdir to throw EEXIST (race condition) vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' }); vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'EEXIST' }); // Should not throw, should return gracefully await expect(mkdirSafe(newDir)).resolves.toBeUndefined(); vi.restoreAllMocks(); }); it('should handle ELOOP error gracefully when creating directory', async () => { const newDir = path.join(testDir, 'eloop-create-dir'); // Mock lstat to return ENOENT (path doesn't exist) // Then mock mkdir to throw ELOOP vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' }); vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'ELOOP' }); // Should not throw, should return gracefully await expect(mkdirSafe(newDir)).resolves.toBeUndefined(); vi.restoreAllMocks(); }); }); describe('existsSafe', () => { it('should return true for existing file', async () => { const filePath = path.join(testDir, 'test-file.txt'); await fs.writeFile(filePath, 'content'); const exists = await existsSafe(filePath); expect(exists).toBe(true); }); it('should return true for existing directory', async () => { const dirPath = path.join(testDir, 'test-dir'); await fs.mkdir(dirPath); const exists = await existsSafe(dirPath); expect(exists).toBe(true); }); it('should return false for non-existent path', async () => { const nonExistent = path.join(testDir, 'does-not-exist'); const exists = await existsSafe(nonExistent); expect(exists).toBe(false); }); it('should return true for symlink', async () => { const realFile = path.join(testDir, 'real-file.txt'); const symlinkPath = path.join(testDir, 'link-to-file'); await fs.writeFile(realFile, 'content'); await fs.symlink(realFile, symlinkPath); const exists = await existsSafe(symlinkPath); expect(exists).toBe(true); }); it("should return true for broken symlink (symlink exists even if target doesn't)", async () => { const symlinkPath = path.join(testDir, 'broken-link'); const nonExistent = path.join(testDir, 'non-existent-target'); await fs.symlink(nonExistent, symlinkPath); const exists = await existsSafe(symlinkPath); expect(exists).toBe(true); }); it('should return true for ELOOP error (symlink loop)', async () => { // Mock lstat to throw ELOOP error vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' }); const exists = await existsSafe('/some/path/with/loop'); expect(exists).toBe(true); vi.restoreAllMocks(); }); it('should throw for other errors', async () => { // Mock lstat to throw a non-ENOENT, non-ELOOP error vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'EACCES' }); await expect(existsSafe('/some/path')).rejects.toMatchObject({ code: 'EACCES' }); vi.restoreAllMocks(); }); }); });