Files
automaker/libs/utils/tests/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

247 lines
7.2 KiB
TypeScript

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