| 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 () => { |
| |
| testDir = path.join(os.tmpdir(), `fs-utils-test-${Date.now()}`); |
| await fs.mkdir(testDir, { recursive: true }); |
| }); |
|
|
| afterEach(async () => { |
| |
| try { |
| await fs.rm(testDir, { recursive: true, force: true }); |
| } catch { |
| |
| } |
| }); |
|
|
| 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); |
|
|
| |
| 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); |
|
|
| |
| await expect(mkdirSafe(symlinkPath)).resolves.toBeUndefined(); |
| }); |
|
|
| it('should handle ELOOP error gracefully when checking path', async () => { |
| |
| const originalLstat = fs.lstat; |
| const mkdirSafePath = path.join(testDir, 'eloop-path'); |
|
|
| vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' }); |
|
|
| |
| 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'); |
|
|
| |
| |
| vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' }); |
| vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'EEXIST' }); |
|
|
| |
| 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'); |
|
|
| |
| |
| vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' }); |
| vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'ELOOP' }); |
|
|
| |
| 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 () => { |
| |
| 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 () => { |
| |
| vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'EACCES' }); |
|
|
| await expect(existsSafe('/some/path')).rejects.toMatchObject({ code: 'EACCES' }); |
|
|
| vi.restoreAllMocks(); |
| }); |
| }); |
| }); |
|
|