Files
automaker/apps/server/tests/unit/lib/fs-utils.test.ts
SuperComboGamer 584f5a3426 Merge main into massive-terminal-upgrade
Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:27:44 -05:00

172 lines
5.7 KiB
TypeScript

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