import { FileListTool } from '../../src/tools/builtin/FileListTool'; import { ToolContext } from '../../src/tools/base/ToolHandler'; import { promises as fs } from 'fs'; import * as path from 'path'; // Mock fs module jest.mock('fs', () => ({ promises: { readdir: jest.fn(), stat: jest.fn(), }, })); describe('FileListTool', () => { let tool: FileListTool; beforeEach(() => { tool = new FileListTool(); jest.clearAllMocks(); }); const createContext = (permissions: string[] = ['file:read']): ToolContext => ({ requestId: 'test-request', permissions, }); describe('execute', () => { it('should list files in a directory', async () => { const testPath = './test/dir'; (fs.readdir as jest.Mock).mockResolvedValue(['file1.txt', 'file2.js', 'subdir']); (fs.stat as jest.Mock) .mockResolvedValueOnce({ isDirectory: () => true }) .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 100, mtime: new Date('2025-01-01'), }) .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 200, mtime: new Date('2025-01-02'), }) .mockResolvedValueOnce({ isDirectory: () => true, isFile: () => false, size: 0, mtime: new Date('2025-01-03'), }); const result = await tool.execute( { path: testPath }, createContext(), ); expect(result.path).toBe(path.resolve(testPath)); const files = result.entries.filter(e => e.type === 'file'); const dirs = result.entries.filter(e => e.type === 'directory'); expect(files).toHaveLength(2); expect(dirs).toHaveLength(1); expect(files[0]).toMatchObject({ name: 'file1.txt', size: 100, }); }); it('should list files recursively', async () => { const testPath = './test/dir'; // Mock for root directory (fs.readdir as jest.Mock) .mockResolvedValueOnce(['file.txt', 'subdir']) .mockResolvedValueOnce(['nested.txt']); (fs.stat as jest.Mock) .mockResolvedValueOnce({ isDirectory: () => true }) // root dir .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 100, mtime: new Date(), }) // file.txt .mockResolvedValueOnce({ isDirectory: () => true, isFile: () => false, size: 0, mtime: new Date(), }) // subdir .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 50, mtime: new Date(), }); // nested.txt const result = await tool.execute( { path: testPath, recursive: true }, createContext(), ); const files = result.entries.filter(e => e.type === 'file'); expect(files).toHaveLength(2); expect(files.map(f => f.path)).toContain(path.resolve(testPath, 'file.txt')); expect(files.map(f => f.path)).toContain(path.resolve(testPath, 'subdir', 'nested.txt')); }); it('should filter by pattern', async () => { const testPath = './test/dir'; (fs.readdir as jest.Mock).mockResolvedValue(['file1.txt', 'file2.js', 'test.txt']); (fs.stat as jest.Mock) .mockResolvedValueOnce({ isDirectory: () => true }) .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 100, mtime: new Date(), }) .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 200, mtime: new Date(), }) .mockResolvedValueOnce({ isDirectory: () => false, isFile: () => true, size: 150, mtime: new Date(), }); const result = await tool.execute( { path: testPath, pattern: '*.txt' }, createContext(), ); const files = result.entries.filter(e => e.type === 'file'); expect(files).toHaveLength(2); expect(files.every(f => f.name.endsWith('.txt'))).toBe(true); }); it('should prevent directory traversal', async () => { await expect( tool.execute( { path: '../../../etc' }, createContext(), ), ).rejects.toThrow('Invalid path: directory traversal not allowed'); }); it('should require file:read permission', async () => { await expect( tool.execute( { path: './test' }, createContext([]), ), ).rejects.toThrow('Permission denied: file:read required'); }); it('should handle non-existent directory', async () => { (fs.stat as jest.Mock).mockResolvedValue({ isDirectory: () => false, isFile: () => false }); await expect( tool.execute( { path: './non/existent' }, createContext(), ), ).rejects.toThrow('Path is not a directory'); }); }); });