We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/bgauryy/local-explorer-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* Tests for local_find_files tool
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { findFiles } from '../../src/tools/local_find_files.js';
import * as exec from '../../src/utils/exec.js';
import * as pathValidator from '../../src/security/pathValidator.js';
// Mock dependencies
vi.mock('../../src/utils/exec.js', () => ({
safeExec: vi.fn(),
}));
vi.mock('../../src/security/pathValidator.js', () => ({
pathValidator: {
validate: vi.fn(),
},
}));
// Mock fs for file details
vi.mock('fs', () => ({
promises: {
lstat: vi.fn(),
},
}));
describe('local_find_files', () => {
const mockSafeExec = vi.mocked(exec.safeExec);
const mockValidate = vi.mocked(pathValidator.pathValidator.validate);
beforeEach(() => {
vi.clearAllMocks();
mockValidate.mockReturnValue({
isValid: true,
sanitizedPath: '/test/path',
});
});
describe('Basic file discovery', () => {
it('should find files by name pattern', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/file1.js\0/test/path/file2.js\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.js',
});
expect(result.status).toBe('hasResults');
expect(result.files).toHaveLength(2);
expect(result.files[0].path).toBe('/test/path/file1.js');
});
it('should handle case-insensitive search', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/FILE.JS\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
iname: '*.js',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-iname', '*.js'])
);
});
it('should handle empty results', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.nonexistent',
});
expect(result.status).toBe('empty');
});
});
describe('File type filtering', () => {
it('should filter by file type', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/file1.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
type: 'f',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-type', 'f'])
);
});
it('should find directories only', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/dir1\0/test/path/dir2\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
type: 'd',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-type', 'd'])
);
});
});
describe('Time-based filtering', () => {
it('should find recently modified files', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/recent.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
modifiedWithin: '7d',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-mtime', '-7'])
);
});
it('should find files modified before date', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/old.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
modifiedBefore: '30d',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-mtime', '+30'])
);
});
});
describe('Size filtering', () => {
it('should find files larger than threshold', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/large.bin\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
sizeGreater: '1M',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-size', '+1M'])
);
});
it('should find files smaller than threshold', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/small.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
sizeLess: '1k',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-size', '-1k'])
);
});
});
describe('Permission filtering', () => {
it('should find executable files', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/script.sh\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
executable: true,
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-executable'])
);
});
it('should filter by permissions', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/file.sh\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
permissions: '755',
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-perm', '755'])
);
});
});
describe('Depth control', () => {
it('should limit search depth', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/file1.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
maxDepth: 2,
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-maxdepth', '2'])
);
});
it('should set minimum depth', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/sub/file.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
minDepth: 1,
});
expect(result.status).toBe('hasResults');
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-mindepth', '1'])
);
});
});
describe('Directory exclusion', () => {
it('should exclude specific directories', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/src/file.js\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
excludeDir: ['node_modules', '.git'],
});
expect(result.status).toBe('hasResults');
// The implementation uses a more complex pattern for excluding directories:
// ( -path */node_modules -o -path */node_modules/* ) -prune -o
expect(mockSafeExec).toHaveBeenCalledWith(
'find',
expect.arrayContaining(['-path', '*/node_modules', '-prune'])
);
});
});
describe('Result limiting', () => {
it('should apply result limit', async () => {
const files = Array.from({ length: 150 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
limit: 50,
});
expect(result.status).toBe('hasResults');
expect(result.files.length).toBeLessThanOrEqual(50);
});
it('should require pagination for large result sets', async () => {
const files = Array.from({ length: 150 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
// No charLength specified
});
// Should either return results or error requesting pagination
expect(['hasResults', 'error']).toContain(result.status);
if (result.status === 'error' && result.error) {
// Error message should indicate size and recommend pagination
expect(result.error.toLowerCase()).toMatch(/char|pagina/i);
}
});
});
describe('Multiple name patterns', () => {
it('should handle multiple name patterns with OR logic', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/path/file1.ts\0/test/path/file2.js\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
names: ['*.ts', '*.js'],
});
expect(result.status).toBe('hasResults');
expect(result.files).toHaveLength(2);
});
});
describe('Path validation', () => {
it('should reject invalid paths', async () => {
mockValidate.mockReturnValue({
isValid: false,
error: 'Path is outside allowed directories',
});
const result = await findFiles({
path: '/etc/passwd',
});
expect(result.status).toBe('error');
expect(result.error).toContain('Path is outside allowed directories');
});
});
describe('Error handling', () => {
it('should handle command failure', async () => {
mockSafeExec.mockResolvedValue({
success: false,
stdout: '',
stderr: 'find: invalid option',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
});
expect(result.status).toBe('error');
});
});
describe('NEW FEATURE: File-based pagination with automatic sorting', () => {
it('should paginate with default 20 files per page', async () => {
const files = Array.from({ length: 50 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
});
expect(result.status).toBe('hasResults');
expect(result.files.length).toBeLessThanOrEqual(20);
expect(result.totalFiles).toBe(50);
expect(result.pagination?.totalPages).toBe(3);
expect(result.pagination?.hasMore).toBe(true);
});
it('should navigate to second page', async () => {
const files = Array.from({ length: 50 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
filePageNumber: 2,
});
expect(result.status).toBe('hasResults');
expect(result.pagination?.currentPage).toBe(2);
expect(result.pagination?.hasMore).toBe(true);
});
it('should support custom filesPerPage', async () => {
const files = Array.from({ length: 50 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
filesPerPage: 10,
});
expect(result.status).toBe('hasResults');
expect(result.files.length).toBeLessThanOrEqual(10);
expect(result.pagination?.filesPerPage).toBe(10);
expect(result.pagination?.totalPages).toBe(5);
});
it('should handle last page correctly', async () => {
const files = Array.from({ length: 25 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
filesPerPage: 20,
filePageNumber: 2,
});
expect(result.status).toBe('hasResults');
expect(result.files.length).toBe(5);
expect(result.pagination?.hasMore).toBe(false);
});
});
describe('NEW FEATURE: ALWAYS sorted by modification time', () => {
it('should ALWAYS sort by modification time (most recent first)', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/old.txt\0/test/new.txt\0/test/mid.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
name: '*.txt',
});
expect(result.status).toBe('hasResults');
expect(result.files.length).toBe(3);
});
it('should sort even with pagination', async () => {
const files = Array.from({ length: 30 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
filesPerPage: 10,
});
expect(result.status).toBe('hasResults');
expect(result.totalFiles).toBe(30);
});
it('should sort with time-based filters', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '/test/recent1.txt\0/test/recent2.txt\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
modifiedWithin: '7d',
});
expect(result.status).toBe('hasResults');
});
});
describe('NEW FEATURE: Pagination hints', () => {
it('should include pagination hints with page info', async () => {
const files = Array.from({ length: 50 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
filesPerPage: 20,
});
expect(result.status).toBe('hasResults');
expect(result.hints).toBeDefined();
expect(result.hints?.some(h => h.includes('Page 1 of 3'))).toBe(true);
expect(result.hints?.some(h => h.includes('filePageNumber=2'))).toBe(true);
});
it('should show final page hint on last page', async () => {
const files = Array.from({ length: 25 }, (_, i) => `/test/file${i}.txt`).join('\0');
mockSafeExec.mockResolvedValue({
success: true,
stdout: files + '\0',
stderr: '',
});
const result = await findFiles({
path: '/test/path',
filesPerPage: 20,
filePageNumber: 2,
});
expect(result.status).toBe('hasResults');
expect(result.hints?.some(h => h.includes('Final page'))).toBe(true);
});
});
});