import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; // Test the actual functions by importing them directly // We'll create a minimal version to test the core logic describe('File Operations', () => { let tempDir: string; let testFile: string; let testDir: string; beforeEach(async () => { // Create a temporary directory for testing tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-test-')); testFile = path.join(tempDir, 'test.txt'); testDir = path.join(tempDir, 'testdir'); // Create test directory await fs.mkdir(testDir); }); afterEach(async () => { // Clean up temporary files try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('formatSize helper function', () => { // Test the formatSize function logic directly it('should format bytes correctly', () => { // Recreate the formatSize function logic for testing function formatSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); if (i === 0) return `${bytes} ${units[i]}`; return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; } const testCases = [ { bytes: 0, expected: '0 B' }, { bytes: 500, expected: '500 B' }, { bytes: 1024, expected: '1.00 KB' }, { bytes: 1536, expected: '1.50 KB' }, { bytes: 1048576, expected: '1.00 MB' }, { bytes: 1073741824, expected: '1.00 GB' } ]; testCases.forEach(testCase => { expect(formatSize(testCase.bytes)).toBe(testCase.expected); }); }); }); describe('headFile function', () => { // Test the head functionality logic async function simulateHeadFile(filePath: string, numLines: number): Promise { const fileHandle = await fs.open(filePath, 'r'); try { const lines: string[] = []; let buffer = ''; let bytesRead = 0; const chunk = Buffer.alloc(1024); while (lines.length < numLines) { const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead); if (result.bytesRead === 0) break; bytesRead += result.bytesRead; buffer += chunk.slice(0, result.bytesRead).toString('utf-8'); const newLineIndex = buffer.lastIndexOf('\n'); if (newLineIndex !== -1) { const completeLines = buffer.slice(0, newLineIndex).split('\n'); buffer = buffer.slice(newLineIndex + 1); for (const line of completeLines) { lines.push(line); if (lines.length >= numLines) break; } } } if (buffer.length > 0 && lines.length < numLines) { lines.push(buffer); } return lines.join('\n'); } finally { await fileHandle.close(); } } it('should read first N lines of a file', async () => { const content = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n'; await fs.writeFile(testFile, content); const result = await simulateHeadFile(testFile, 3); expect(result).toBe('Line 1\nLine 2\nLine 3'); }); it('should handle empty files', async () => { await fs.writeFile(testFile, ''); const result = await simulateHeadFile(testFile, 3); expect(result).toBe(''); }); it('should handle files with fewer lines than requested', async () => { const content = 'Line 1\nLine 2'; await fs.writeFile(testFile, content); const result = await simulateHeadFile(testFile, 5); expect(result).toBe('Line 1\nLine 2'); }); }); describe('tailFile function', () => { // Test the tail functionality logic async function simulateTailFile(filePath: string, numLines: number): Promise { function normalizeLineEndings(text: string): string { return text.replace(/\r\n/g, '\n'); } const CHUNK_SIZE = 1024; const stats = await fs.stat(filePath); const fileSize = stats.size; if (fileSize === 0) return ''; const fileHandle = await fs.open(filePath, 'r'); try { const lines: string[] = []; let position = fileSize; let chunk = Buffer.alloc(CHUNK_SIZE); let linesFound = 0; let remainingText = ''; while (position > 0 && linesFound < numLines) { const size = Math.min(CHUNK_SIZE, position); position -= size; const { bytesRead } = await fileHandle.read(chunk, 0, size, position); if (!bytesRead) break; const readData = chunk.slice(0, bytesRead).toString('utf-8'); const chunkText = readData + remainingText; const chunkLines = normalizeLineEndings(chunkText).split('\n'); if (position > 0) { remainingText = chunkLines[0]; chunkLines.shift(); } for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) { lines.unshift(chunkLines[i]); linesFound++; } } return lines.join('\n'); } finally { await fileHandle.close(); } } it('should read last N lines of a file', async () => { const content = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'; await fs.writeFile(testFile, content); const result = await simulateTailFile(testFile, 3); expect(result).toBe('Line 3\nLine 4\nLine 5'); }); it('should handle empty files', async () => { await fs.writeFile(testFile, ''); const result = await simulateTailFile(testFile, 3); expect(result).toBe(''); }); }); describe('head and tail parameter validation', () => { it('should reject when both head and tail are specified', () => { // This tests the validation logic that should throw an error // when both parameters are provided const hasHead = true; const hasTail = true; if (hasHead && hasTail) { expect(() => { throw new Error("Cannot specify both head and tail parameters simultaneously"); }).toThrow("Cannot specify both head and tail parameters simultaneously"); } }); }); describe('list_directory_with_sizes functionality', () => { it('should list directory contents with sizes', async () => { // Create test files with known content await fs.writeFile(path.join(testDir, 'small.txt'), 'hello'); await fs.writeFile(path.join(testDir, 'large.txt'), 'x'.repeat(1024)); await fs.mkdir(path.join(testDir, 'subdir')); // Read the directory and verify structure const entries = await fs.readdir(testDir, { withFileTypes: true }); expect(entries.length).toBe(3); const fileNames = entries.map(e => e.name).sort(); expect(fileNames).toEqual(['large.txt', 'small.txt', 'subdir']); // Verify we can distinguish files from directories const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); const files = entries.filter(e => e.isFile()).map(e => e.name); expect(dirs).toEqual(['subdir']); expect(files.sort()).toEqual(['large.txt', 'small.txt']); }); it('should handle sorting by name and size', async () => { // Create files with different sizes await fs.writeFile(path.join(testDir, 'big.txt'), 'x'.repeat(2000)); await fs.writeFile(path.join(testDir, 'small.txt'), 'hi'); await fs.writeFile(path.join(testDir, 'medium.txt'), 'x'.repeat(1000)); const entries = await fs.readdir(testDir, { withFileTypes: true }); // Get file sizes const filesWithSizes = await Promise.all( entries.filter(e => e.isFile()).map(async (entry) => { const stats = await fs.stat(path.join(testDir, entry.name)); return { name: entry.name, size: stats.size }; }) ); // Test name sorting const sortedByName = [...filesWithSizes].sort((a, b) => a.name.localeCompare(b.name)); expect(sortedByName.map(f => f.name)).toEqual(['big.txt', 'medium.txt', 'small.txt']); // Test size sorting (descending) const sortedBySize = [...filesWithSizes].sort((a, b) => b.size - a.size); expect(sortedBySize.map(f => f.name)).toEqual(['big.txt', 'medium.txt', 'small.txt']); }); }); });