File size: 5,786 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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();
    });
  });
});