Skip to main content
Glama

Knowledge Graph Memory Server

MIT License
52,555
68,825
  • Apple
  • Linux
lib.test.ts25 kB
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { // Pure utility functions formatSize, normalizeLineEndings, createUnifiedDiff, // Security & validation functions validatePath, setAllowedDirectories, // File operations getFileStats, readFileContent, writeFileContent, // Search & filtering functions searchFilesWithValidation, // File editing functions applyFileEdits, tailFile, headFile } from '../lib.js'; // Mock fs module jest.mock('fs/promises'); const mockFs = fs as jest.Mocked<typeof fs>; describe('Lib Functions', () => { beforeEach(() => { jest.clearAllMocks(); // Set up allowed directories for tests const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp', 'C:\\allowed'] : ['/home/user', '/tmp', '/allowed']; setAllowedDirectories(allowedDirs); }); afterEach(() => { jest.restoreAllMocks(); // Clear allowed directories after tests setAllowedDirectories([]); }); describe('Pure Utility Functions', () => { describe('formatSize', () => { it('formats bytes correctly', () => { expect(formatSize(0)).toBe('0 B'); expect(formatSize(512)).toBe('512 B'); expect(formatSize(1024)).toBe('1.00 KB'); expect(formatSize(1536)).toBe('1.50 KB'); expect(formatSize(1048576)).toBe('1.00 MB'); expect(formatSize(1073741824)).toBe('1.00 GB'); expect(formatSize(1099511627776)).toBe('1.00 TB'); }); it('handles edge cases', () => { expect(formatSize(1023)).toBe('1023 B'); expect(formatSize(1025)).toBe('1.00 KB'); expect(formatSize(1048575)).toBe('1024.00 KB'); }); it('handles very large numbers beyond TB', () => { // The function only supports up to TB, so very large numbers will show as TB expect(formatSize(1024 * 1024 * 1024 * 1024 * 1024)).toBe('1024.00 TB'); expect(formatSize(Number.MAX_SAFE_INTEGER)).toContain('TB'); }); it('handles negative numbers', () => { // Negative numbers will result in NaN for the log calculation expect(formatSize(-1024)).toContain('NaN'); expect(formatSize(-0)).toBe('0 B'); }); it('handles decimal numbers', () => { expect(formatSize(1536.5)).toBe('1.50 KB'); expect(formatSize(1023.9)).toBe('1023.9 B'); }); it('handles very small positive numbers', () => { expect(formatSize(1)).toBe('1 B'); expect(formatSize(0.5)).toBe('0.5 B'); expect(formatSize(0.1)).toBe('0.1 B'); }); }); describe('normalizeLineEndings', () => { it('converts CRLF to LF', () => { expect(normalizeLineEndings('line1\r\nline2\r\nline3')).toBe('line1\nline2\nline3'); }); it('leaves LF unchanged', () => { expect(normalizeLineEndings('line1\nline2\nline3')).toBe('line1\nline2\nline3'); }); it('handles mixed line endings', () => { expect(normalizeLineEndings('line1\r\nline2\nline3\r\n')).toBe('line1\nline2\nline3\n'); }); it('handles empty string', () => { expect(normalizeLineEndings('')).toBe(''); }); }); describe('createUnifiedDiff', () => { it('creates diff for simple changes', () => { const original = 'line1\nline2\nline3'; const modified = 'line1\nmodified line2\nline3'; const diff = createUnifiedDiff(original, modified, 'test.txt'); expect(diff).toContain('--- test.txt'); expect(diff).toContain('+++ test.txt'); expect(diff).toContain('-line2'); expect(diff).toContain('+modified line2'); }); it('handles CRLF normalization', () => { const original = 'line1\r\nline2\r\n'; const modified = 'line1\nmodified line2\n'; const diff = createUnifiedDiff(original, modified); expect(diff).toContain('-line2'); expect(diff).toContain('+modified line2'); }); it('handles identical content', () => { const content = 'line1\nline2\nline3'; const diff = createUnifiedDiff(content, content); // Should not contain any +/- lines for identical content (excluding header lines) expect(diff.split('\n').filter((line: string) => line.startsWith('+++') || line.startsWith('---'))).toHaveLength(2); expect(diff.split('\n').filter((line: string) => line.startsWith('+') && !line.startsWith('+++'))).toHaveLength(0); expect(diff.split('\n').filter((line: string) => line.startsWith('-') && !line.startsWith('---'))).toHaveLength(0); }); it('handles empty content', () => { const diff = createUnifiedDiff('', ''); expect(diff).toContain('--- file'); expect(diff).toContain('+++ file'); }); it('handles default filename parameter', () => { const diff = createUnifiedDiff('old', 'new'); expect(diff).toContain('--- file'); expect(diff).toContain('+++ file'); }); it('handles custom filename', () => { const diff = createUnifiedDiff('old', 'new', 'custom.txt'); expect(diff).toContain('--- custom.txt'); expect(diff).toContain('+++ custom.txt'); }); }); }); describe('Security & Validation Functions', () => { describe('validatePath', () => { // Use Windows-compatible paths for testing const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp'] : ['/home/user', '/tmp']; beforeEach(() => { mockFs.realpath.mockImplementation(async (path: any) => path.toString()); }); it('validates allowed paths', async () => { const testPath = process.platform === 'win32' ? 'C:\\Users\\test\\file.txt' : '/home/user/file.txt'; const result = await validatePath(testPath); expect(result).toBe(testPath); }); it('rejects disallowed paths', async () => { const testPath = process.platform === 'win32' ? 'C:\\Windows\\System32\\file.txt' : '/etc/passwd'; await expect(validatePath(testPath)) .rejects.toThrow('Access denied - path outside allowed directories'); }); it('handles non-existent files by checking parent directory', async () => { const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\newfile.txt' : '/home/user/newfile.txt'; const parentPath = process.platform === 'win32' ? 'C:\\Users\\test' : '/home/user'; // Create an error with the ENOENT code that the implementation checks for const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; enoentError.code = 'ENOENT'; mockFs.realpath .mockRejectedValueOnce(enoentError) .mockResolvedValueOnce(parentPath); const result = await validatePath(newFilePath); expect(result).toBe(path.resolve(newFilePath)); }); it('rejects when parent directory does not exist', async () => { const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\nonexistent\\newfile.txt' : '/home/user/nonexistent/newfile.txt'; // Create errors with the ENOENT code const enoentError1 = new Error('ENOENT') as NodeJS.ErrnoException; enoentError1.code = 'ENOENT'; const enoentError2 = new Error('ENOENT') as NodeJS.ErrnoException; enoentError2.code = 'ENOENT'; mockFs.realpath .mockRejectedValueOnce(enoentError1) .mockRejectedValueOnce(enoentError2); await expect(validatePath(newFilePath)) .rejects.toThrow('Parent directory does not exist'); }); }); }); describe('File Operations', () => { describe('getFileStats', () => { it('returns file statistics', async () => { const mockStats = { size: 1024, birthtime: new Date('2023-01-01'), mtime: new Date('2023-01-02'), atime: new Date('2023-01-03'), isDirectory: () => false, isFile: () => true, mode: 0o644 }; mockFs.stat.mockResolvedValueOnce(mockStats as any); const result = await getFileStats('/test/file.txt'); expect(result).toEqual({ size: 1024, created: new Date('2023-01-01'), modified: new Date('2023-01-02'), accessed: new Date('2023-01-03'), isDirectory: false, isFile: true, permissions: '644' }); }); it('handles directory statistics', async () => { const mockStats = { size: 4096, birthtime: new Date('2023-01-01'), mtime: new Date('2023-01-02'), atime: new Date('2023-01-03'), isDirectory: () => true, isFile: () => false, mode: 0o755 }; mockFs.stat.mockResolvedValueOnce(mockStats as any); const result = await getFileStats('/test/dir'); expect(result.isDirectory).toBe(true); expect(result.isFile).toBe(false); expect(result.permissions).toBe('755'); }); }); describe('readFileContent', () => { it('reads file with default encoding', async () => { mockFs.readFile.mockResolvedValueOnce('file content'); const result = await readFileContent('/test/file.txt'); expect(result).toBe('file content'); expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'utf-8'); }); it('reads file with custom encoding', async () => { mockFs.readFile.mockResolvedValueOnce('file content'); const result = await readFileContent('/test/file.txt', 'ascii'); expect(result).toBe('file content'); expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'ascii'); }); }); describe('writeFileContent', () => { it('writes file content', async () => { mockFs.writeFile.mockResolvedValueOnce(undefined); await writeFileContent('/test/file.txt', 'new content'); expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'new content', { encoding: "utf-8", flag: 'wx' }); }); }); }); describe('Search & Filtering Functions', () => { describe('searchFilesWithValidation', () => { beforeEach(() => { mockFs.realpath.mockImplementation(async (path: any) => path.toString()); }); it('excludes files matching exclude patterns', async () => { const mockEntries = [ { name: 'test.txt', isDirectory: () => false }, { name: 'test.log', isDirectory: () => false }, { name: 'node_modules', isDirectory: () => true } ]; mockFs.readdir.mockResolvedValueOnce(mockEntries as any); const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; // Mock realpath to return the same path for validation to pass mockFs.realpath.mockImplementation(async (inputPath: any) => { const pathStr = inputPath.toString(); // Return the path as-is for validation return pathStr; }); const result = await searchFilesWithValidation( testDir, '*test*', allowedDirs, { excludePatterns: ['*.log', 'node_modules'] } ); const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt'; expect(result).toEqual([expectedResult]); }); it('handles validation errors during search', async () => { const mockEntries = [ { name: 'test.txt', isDirectory: () => false }, { name: 'invalid_file.txt', isDirectory: () => false } ]; mockFs.readdir.mockResolvedValueOnce(mockEntries as any); // Mock validatePath to throw error for invalid_file.txt mockFs.realpath.mockImplementation(async (path: any) => { if (path.toString().includes('invalid_file.txt')) { throw new Error('Access denied'); } return path.toString(); }); const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; const result = await searchFilesWithValidation( testDir, '*test*', allowedDirs, {} ); // Should only return the valid file, skipping the invalid one const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt'; expect(result).toEqual([expectedResult]); }); it('handles complex exclude patterns with wildcards', async () => { const mockEntries = [ { name: 'test.txt', isDirectory: () => false }, { name: 'test.backup', isDirectory: () => false }, { name: 'important_test.js', isDirectory: () => false } ]; mockFs.readdir.mockResolvedValueOnce(mockEntries as any); const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; const result = await searchFilesWithValidation( testDir, '*test*', allowedDirs, { excludePatterns: ['*.backup'] } ); const expectedResults = process.platform === 'win32' ? [ 'C:\\allowed\\dir\\test.txt', 'C:\\allowed\\dir\\important_test.js' ] : [ '/allowed/dir/test.txt', '/allowed/dir/important_test.js' ]; expect(result).toEqual(expectedResults); }); }); }); describe('File Editing Functions', () => { describe('applyFileEdits', () => { beforeEach(() => { mockFs.readFile.mockResolvedValue('line1\nline2\nline3\n'); mockFs.writeFile.mockResolvedValue(undefined); }); it('applies simple text replacement', async () => { const edits = [ { oldText: 'line2', newText: 'modified line2' } ]; mockFs.rename.mockResolvedValueOnce(undefined); const result = await applyFileEdits('/test/file.txt', edits, false); expect(result).toContain('modified line2'); // Should write to temporary file then rename expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), 'line1\nmodified line2\nline3\n', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), '/test/file.txt' ); }); it('handles dry run mode', async () => { const edits = [ { oldText: 'line2', newText: 'modified line2' } ]; const result = await applyFileEdits('/test/file.txt', edits, true); expect(result).toContain('modified line2'); expect(mockFs.writeFile).not.toHaveBeenCalled(); }); it('applies multiple edits sequentially', async () => { const edits = [ { oldText: 'line1', newText: 'first line' }, { oldText: 'line3', newText: 'third line' } ]; mockFs.rename.mockResolvedValueOnce(undefined); await applyFileEdits('/test/file.txt', edits, false); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), 'first line\nline2\nthird line\n', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), '/test/file.txt' ); }); it('handles whitespace-flexible matching', async () => { mockFs.readFile.mockResolvedValue(' line1\n line2\n line3\n'); const edits = [ { oldText: 'line2', newText: 'modified line2' } ]; mockFs.rename.mockResolvedValueOnce(undefined); await applyFileEdits('/test/file.txt', edits, false); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), ' line1\n modified line2\n line3\n', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), '/test/file.txt' ); }); it('throws error for non-matching edits', async () => { const edits = [ { oldText: 'nonexistent line', newText: 'replacement' } ]; await expect(applyFileEdits('/test/file.txt', edits, false)) .rejects.toThrow('Could not find exact match for edit'); }); it('handles complex multi-line edits with indentation', async () => { mockFs.readFile.mockResolvedValue('function test() {\n console.log("hello");\n return true;\n}'); const edits = [ { oldText: ' console.log("hello");\n return true;', newText: ' console.log("world");\n console.log("test");\n return false;' } ]; mockFs.rename.mockResolvedValueOnce(undefined); await applyFileEdits('/test/file.js', edits, false); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), 'function test() {\n console.log("world");\n console.log("test");\n return false;\n}', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), '/test/file.js' ); }); it('handles edits with different indentation patterns', async () => { mockFs.readFile.mockResolvedValue(' if (condition) {\n doSomething();\n }'); const edits = [ { oldText: 'doSomething();', newText: 'doSomethingElse();\n doAnotherThing();' } ]; mockFs.rename.mockResolvedValueOnce(undefined); await applyFileEdits('/test/file.js', edits, false); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), ' if (condition) {\n doSomethingElse();\n doAnotherThing();\n }', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), '/test/file.js' ); }); it('handles CRLF line endings in file content', async () => { mockFs.readFile.mockResolvedValue('line1\r\nline2\r\nline3\r\n'); const edits = [ { oldText: 'line2', newText: 'modified line2' } ]; mockFs.rename.mockResolvedValueOnce(undefined); await applyFileEdits('/test/file.txt', edits, false); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), 'line1\nmodified line2\nline3\n', 'utf-8' ); expect(mockFs.rename).toHaveBeenCalledWith( expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), '/test/file.txt' ); }); }); describe('tailFile', () => { it('handles empty files', async () => { mockFs.stat.mockResolvedValue({ size: 0 } as any); const result = await tailFile('/test/empty.txt', 5); expect(result).toBe(''); expect(mockFs.open).not.toHaveBeenCalled(); }); it('calls stat to check file size', async () => { mockFs.stat.mockResolvedValue({ size: 100 } as any); // Mock file handle with proper typing const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); await tailFile('/test/file.txt', 2); expect(mockFs.stat).toHaveBeenCalledWith('/test/file.txt'); expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r'); }); it('handles files with content and returns last lines', async () => { mockFs.stat.mockResolvedValue({ size: 50 } as any); const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; // Simulate reading file content in chunks mockFileHandle.read .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line3\nline4\nline5\n') }) .mockResolvedValueOnce({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); const result = await tailFile('/test/file.txt', 2); expect(mockFileHandle.close).toHaveBeenCalled(); }); it('handles read errors gracefully', async () => { mockFs.stat.mockResolvedValue({ size: 100 } as any); const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); await tailFile('/test/file.txt', 5); expect(mockFileHandle.close).toHaveBeenCalled(); }); }); describe('headFile', () => { it('opens file for reading', async () => { // Mock file handle with proper typing const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); await headFile('/test/file.txt', 2); expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r'); }); it('handles files with content and returns first lines', async () => { const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; // Simulate reading file content with newlines mockFileHandle.read .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line1\nline2\nline3\n') }) .mockResolvedValueOnce({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); const result = await headFile('/test/file.txt', 2); expect(mockFileHandle.close).toHaveBeenCalled(); }); it('handles files with leftover content', async () => { const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; // Simulate reading file content without final newline mockFileHandle.read .mockResolvedValueOnce({ bytesRead: 15, buffer: Buffer.from('line1\nline2\nend') }) .mockResolvedValueOnce({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); const result = await headFile('/test/file.txt', 5); expect(mockFileHandle.close).toHaveBeenCalled(); }); it('handles reaching requested line count', async () => { const mockFileHandle = { read: jest.fn(), close: jest.fn() } as any; // Simulate reading exactly the requested number of lines mockFileHandle.read .mockResolvedValueOnce({ bytesRead: 12, buffer: Buffer.from('line1\nline2\n') }) .mockResolvedValueOnce({ bytesRead: 0 }); mockFileHandle.close.mockResolvedValue(undefined); mockFs.open.mockResolvedValue(mockFileHandle); const result = await headFile('/test/file.txt', 2); expect(mockFileHandle.close).toHaveBeenCalled(); }); }); }); });

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/modelcontextprotocol/knowledge-graph-memory-server'

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