mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 22:32:04 +00:00
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>
247 lines
7.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|