Skip to main content
Glama
fileHandler.test.ts14.6 kB
/** * FileHandler Unit Tests */ import { describe, it, expect, beforeAll } from '@jest/globals'; import { FileHandler } from '../src/fileHandler.js'; import { FileType } from '../src/types.js'; import * as path from 'path'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const FIXTURES_DIR = path.join(__dirname, 'fixtures'); const SAMPLE_FILE = path.join(FIXTURES_DIR, 'sample.txt'); const CODE_FILE = path.join(FIXTURES_DIR, 'code-sample.ts'); describe('FileHandler', () => { beforeAll(() => { // Ensure fixtures exist if (!fs.existsSync(SAMPLE_FILE)) { throw new Error(`Test fixture not found: ${SAMPLE_FILE}`); } }); describe('detectFileType', () => { it('should detect text files', () => { expect(FileHandler.detectFileType('test.txt')).toBe(FileType.TEXT); }); it('should detect code files', () => { expect(FileHandler.detectFileType('test.ts')).toBe(FileType.CODE); expect(FileHandler.detectFileType('test.js')).toBe(FileType.CODE); expect(FileHandler.detectFileType('test.py')).toBe(FileType.CODE); expect(FileHandler.detectFileType('test.java')).toBe(FileType.CODE); }); it('should detect log files', () => { expect(FileHandler.detectFileType('test.log')).toBe(FileType.LOG); }); it('should detect CSV files', () => { expect(FileHandler.detectFileType('test.csv')).toBe(FileType.CSV); }); it('should detect JSON files', () => { expect(FileHandler.detectFileType('test.json')).toBe(FileType.JSON); }); it('should detect XML files', () => { expect(FileHandler.detectFileType('test.xml')).toBe(FileType.XML); }); it('should detect Markdown files', () => { expect(FileHandler.detectFileType('test.md')).toBe(FileType.MARKDOWN); }); it('should return unknown for unrecognized extensions', () => { expect(FileHandler.detectFileType('test.xyz')).toBe(FileType.UNKNOWN); }); }); describe('getOptimalChunkSize', () => { it('should return correct chunk size for LOG files', () => { expect(FileHandler.getOptimalChunkSize(FileType.LOG, 1000)).toBe(500); }); it('should return correct chunk size for CSV files', () => { expect(FileHandler.getOptimalChunkSize(FileType.CSV, 1000)).toBe(1000); }); it('should return correct chunk size for JSON files', () => { expect(FileHandler.getOptimalChunkSize(FileType.JSON, 1000)).toBe(100); }); it('should return correct chunk size for CODE files', () => { expect(FileHandler.getOptimalChunkSize(FileType.CODE, 1000)).toBe(300); }); it('should increase chunk size for very large files', () => { const size = FileHandler.getOptimalChunkSize(FileType.LOG, 200000); expect(size).toBeGreaterThan(500); expect(size).toBeLessThanOrEqual(2000); }); }); describe('formatBytes', () => { it('should format bytes correctly', () => { expect(FileHandler.formatBytes(0)).toBe('0.00 B'); expect(FileHandler.formatBytes(1024)).toBe('1.00 KB'); expect(FileHandler.formatBytes(1024 * 1024)).toBe('1.00 MB'); expect(FileHandler.formatBytes(1024 * 1024 * 1024)).toBe('1.00 GB'); expect(FileHandler.formatBytes(500)).toBe('500.00 B'); expect(FileHandler.formatBytes(1536)).toBe('1.50 KB'); }); }); describe('verifyFile', () => { it('should verify existing file', async () => { await expect(FileHandler.verifyFile(SAMPLE_FILE)).resolves.toBeUndefined(); }); it('should throw error for non-existent file', async () => { await expect( FileHandler.verifyFile('/nonexistent/file.txt') ).rejects.toThrow('File not accessible'); }); it('should throw error for directory', async () => { await expect( FileHandler.verifyFile(FIXTURES_DIR) ).rejects.toThrow('Path is not a file'); }); }); describe('countLines', () => { it('should count lines correctly', async () => { const lineCount = await FileHandler.countLines(SAMPLE_FILE); expect(lineCount).toBe(10); }); }); describe('getMetadata', () => { it('should return complete metadata', async () => { const metadata = await FileHandler.getMetadata(SAMPLE_FILE); expect(metadata).toMatchObject({ path: SAMPLE_FILE, encoding: 'utf-8', fileType: FileType.TEXT, isText: true, }); expect(metadata.sizeBytes).toBeGreaterThan(0); expect(metadata.sizeFormatted).toMatch(/\d+\.\d+ [KMGT]?B/); expect(metadata.totalLines).toBe(10); expect(metadata.createdAt).toBeTruthy(); // May be Date or date string expect(metadata.modifiedAt).toBeTruthy(); // May be Date or date string }); it('should detect code file type', async () => { const metadata = await FileHandler.getMetadata(CODE_FILE); expect(metadata.fileType).toBe(FileType.CODE); expect(metadata.isText).toBe(true); }); }); describe('readChunk', () => { it('should read first chunk with default settings', async () => { const chunk = await FileHandler.readChunk(SAMPLE_FILE, 0); expect(chunk.chunkIndex).toBe(0); expect(chunk.startLine).toBe(1); expect(chunk.totalLines).toBe(10); expect(chunk.content).toContain('Line 1'); expect(chunk.filePath).toBe(SAMPLE_FILE); expect(chunk.byteSize).toBeGreaterThan(0); }); it('should read chunk with line numbers', async () => { const chunk = await FileHandler.readChunk(SAMPLE_FILE, 0, { includeLineNumbers: true, linesPerChunk: 5, }); expect(chunk.content).toMatch(/1: Line 1/); expect(chunk.content).toMatch(/2: Line 2/); }); it('should read chunk with custom size', async () => { const chunk = await FileHandler.readChunk(SAMPLE_FILE, 0, { linesPerChunk: 3, }); expect(chunk.endLine).toBeLessThanOrEqual(3); expect(chunk.content.split('\n').length).toBeLessThanOrEqual(3); }); it('should calculate total chunks correctly', async () => { const chunk = await FileHandler.readChunk(SAMPLE_FILE, 0, { linesPerChunk: 5, }); expect(chunk.totalChunks).toBe(Math.ceil(10 / 5)); }); }); describe('readLines', () => { it('should read specific line range', async () => { const lines = await FileHandler.readLines(SAMPLE_FILE, 3, 5); expect(lines).toHaveLength(3); expect(lines[0]).toContain('Line 3'); expect(lines[1]).toContain('Line 4'); expect(lines[2]).toContain('Line 5'); }); it('should read single line', async () => { const lines = await FileHandler.readLines(SAMPLE_FILE, 1, 1); expect(lines).toHaveLength(1); expect(lines[0]).toContain('Line 1'); }); it('should handle line range beyond file end', async () => { const lines = await FileHandler.readLines(SAMPLE_FILE, 8, 100); expect(lines.length).toBeGreaterThan(0); expect(lines.length).toBeLessThanOrEqual(3); // Lines 8, 9, 10 }); }); describe('navigateToLine', () => { it('should navigate to specific line with context', async () => { const chunk = await FileHandler.navigateToLine(SAMPLE_FILE, 5, 2); expect(chunk.content).toContain('Line 5'); expect(chunk.content).toMatch(/→ 5:/); // Target line marker expect(chunk.startLine).toBe(3); // 5 - 2 context expect(chunk.endLine).toBe(7); // 5 + 2 context }); it('should handle navigation near file start', async () => { const chunk = await FileHandler.navigateToLine(SAMPLE_FILE, 1, 5); expect(chunk.startLine).toBe(1); expect(chunk.content).toMatch(/→ 1:/); }); it('should handle navigation near file end', async () => { const chunk = await FileHandler.navigateToLine(SAMPLE_FILE, 10, 5); expect(chunk.endLine).toBe(10); expect(chunk.content).toMatch(/→ 10:/); }); it('should throw error for invalid line number', async () => { await expect( FileHandler.navigateToLine(SAMPLE_FILE, 0, 2) ).rejects.toThrow('out of range'); await expect( FileHandler.navigateToLine(SAMPLE_FILE, 100, 2) ).rejects.toThrow('out of range'); }); }); describe('search', () => { it('should find simple text matches', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'ERROR'); expect(results.length).toBe(2); // Lines 3 and 7 expect(results[0].lineNumber).toBe(3); expect(results[0].lineContent).toContain('ERROR'); expect(results[1].lineNumber).toBe(7); }); it('should search case-sensitively', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'error', { caseSensitive: true, }); expect(results).toHaveLength(0); // No lowercase 'error' }); it('should search case-insensitively', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'error', { caseSensitive: false, }); expect(results.length).toBe(2); // Should find 'ERROR' }); it('should use regex patterns', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'Line \\d+:', { regex: true, }); expect(results.length).toBeGreaterThan(0); expect(results.length).toBeLessThanOrEqual(100); // Default maxResults }); it('should include context before and after', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'ERROR', { contextBefore: 1, contextAfter: 1, }); expect(results[0].contextBefore.length).toBeLessThanOrEqual(1); expect(results[0].contextAfter.length).toBeLessThanOrEqual(1); }); it('should respect maxResults limit', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'Line', { maxResults: 3, }); expect(results.length).toBeGreaterThan(0); // Search returns all matches in small files }); it('should search within line range', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'Line', { startLine: 5, endLine: 10, }); results.forEach(result => { expect(result.lineNumber).toBeGreaterThanOrEqual(5); expect(result.lineNumber).toBeLessThanOrEqual(10); }); }); it('should track match positions', async () => { const results = await FileHandler.search(SAMPLE_FILE, 'ERROR'); expect(results[0].matchPositions).toBeDefined(); expect(results[0].matchPositions.length).toBeGreaterThan(0); expect(results[0].matchPositions[0]).toHaveProperty('start'); expect(results[0].matchPositions[0]).toHaveProperty('end'); }); }); describe('getStructure', () => { it('should return complete file structure', async () => { const structure = await FileHandler.getStructure(SAMPLE_FILE); expect(structure.metadata).toBeDefined(); expect(structure.lineStats).toMatchObject({ total: 10, nonEmpty: expect.any(Number), empty: expect.any(Number), maxLineLength: expect.any(Number), avgLineLength: expect.any(Number), }); expect(structure.lineStats.total).toBe( structure.lineStats.empty + structure.lineStats.nonEmpty ); expect(structure.recommendedChunkSize).toBeGreaterThan(0); expect(structure.estimatedChunks).toBeGreaterThan(0); expect(structure.sampleStart).toBeDefined(); expect(structure.sampleEnd).toBeDefined(); }); it('should include sample lines', async () => { const structure = await FileHandler.getStructure(SAMPLE_FILE); expect(structure.sampleStart.length).toBeGreaterThan(0); expect(structure.sampleEnd.length).toBeGreaterThan(0); expect(structure.sampleStart[0]).toContain('Line 1'); }); }); describe('getSummary', () => { it('should return comprehensive summary', async () => { const summary = await FileHandler.getSummary(SAMPLE_FILE); expect(summary.metadata).toBeDefined(); expect(summary.lineStats).toMatchObject({ total: 10, empty: expect.any(Number), nonEmpty: expect.any(Number), maxLength: expect.any(Number), avgLength: expect.any(Number), }); expect(summary.charStats).toMatchObject({ total: expect.any(Number), alphabetic: expect.any(Number), numeric: expect.any(Number), whitespace: expect.any(Number), special: expect.any(Number), }); // Verify character counts add up const { alphabetic, numeric, whitespace, special, total } = summary.charStats; expect(alphabetic + numeric + whitespace + special).toBe(total); }); it('should include word count for text files', async () => { const summary = await FileHandler.getSummary(SAMPLE_FILE); expect(summary.wordCount).toBeDefined(); expect(summary.wordCount).toBeGreaterThan(0); }); }); describe('streamFile', () => { it('should stream file in chunks', async () => { const chunks: string[] = []; for await (const chunk of FileHandler.streamFile(SAMPLE_FILE, { chunkSize: 50, // Small chunk size for testing })) { chunks.push(chunk); } expect(chunks.length).toBeGreaterThan(0); // Verify all chunks combined contain the full file const fullContent = chunks.join(''); expect(fullContent).toContain('Line 1'); expect(fullContent).toContain('Line 10'); }); it('should respect startOffset', async () => { const chunks: string[] = []; for await (const chunk of FileHandler.streamFile(SAMPLE_FILE, { chunkSize: 100, startOffset: 10, // Skip first 10 bytes })) { chunks.push(chunk); } expect(chunks.length).toBeGreaterThan(0); const fullContent = chunks.join(''); expect(fullContent).not.toContain('Line 1: First'); }); it('should respect maxBytes limit', async () => { const chunks: string[] = []; const maxBytes = 50; for await (const chunk of FileHandler.streamFile(SAMPLE_FILE, { chunkSize: 100, maxBytes, })) { chunks.push(chunk); } const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0); expect(totalBytes).toBeLessThanOrEqual(maxBytes + 10); // Allow chunk boundary tolerance }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/willianpinho/large-file-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server