mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-19 00:23:24 +02:00
253 lines
8.5 KiB
TypeScript
253 lines
8.5 KiB
TypeScript
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<string> {
|
|
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<string> {
|
|
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']);
|
|
});
|
|
});
|
|
});
|