Skip to main content
Glama
listFiles.property.test.ts9.74 kB
/** * Property-based tests for list_files tool * Tests file listing completeness and recursive listing */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fc from 'fast-check'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { executeListFiles } from '../src/tools/listFiles.js'; import { ServerConfig } from '../src/config.js'; describe('list_files Tool - Property Tests', () => { let testWorkspace: string; let config: ServerConfig; beforeEach(async () => { // Create a temporary workspace for testing const tempBase = path.join(os.tmpdir(), 'listfiles-pbt-' + Date.now()); await fs.mkdir(tempBase, { recursive: true }); testWorkspace = tempBase; config = { workspaceRoot: testWorkspace, allowedCommands: [], readOnly: false, logLevel: 'error', commandTimeout: 300000, }; }); afterEach(async () => { // Clean up temporary workspace try { await fs.rm(testWorkspace, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); // Generator for valid directory/file names const validNameGenerator = () => fc .stringMatching(/^[a-zA-Z0-9_-]+$/) .filter(s => s.length > 0 && s.length <= 20); // Generator for file content const fileContentGenerator = () => fc.string({ minLength: 0, maxLength: 100 }); /** * Property 2: File listing completeness * For any directory in the workspace, listing that directory should return * all files and directories it contains, each with name, relative path, type, * size (for files), and last modified timestamp. * * Feature: mcp-workspace-server, Property 2: File listing completeness * Validates: Requirements 1.1 */ it('Property 2: should return all entries with correct metadata', async () => { // Generator for a flat directory structure with unique names const flatStructureGenerator = fc.record({ files: fc.uniqueArray( fc.record({ name: validNameGenerator().map(n => n + '.txt'), content: fileContentGenerator(), }), { minLength: 0, maxLength: 10, selector: (item) => item.name } ), directories: fc.uniqueArray( validNameGenerator(), { minLength: 0, maxLength: 5 } ), }); await fc.assert( fc.asyncProperty(flatStructureGenerator, async (structure) => { // Create a unique subdirectory for this test iteration const testSubdir = path.join(testWorkspace, `test-${Date.now()}-${Math.random().toString(36).substring(7)}`); await fs.mkdir(testSubdir, { recursive: true }); // Create the directory structure const createdFiles = new Map<string, string>(); // name -> content const createdDirs = new Set<string>(); // Create files for (const file of structure.files) { const filePath = path.join(testSubdir, file.name); await fs.writeFile(filePath, file.content, 'utf-8'); createdFiles.set(file.name, file.content); } // Create directories (skip if name conflicts with a file) for (const dir of structure.directories) { if (!createdFiles.has(dir)) { const dirPath = path.join(testSubdir, dir); await fs.mkdir(dirPath, { recursive: true }); createdDirs.add(dir); } } // List the directory (use relative path from workspace root) const relativePath = path.relative(testWorkspace, testSubdir); const result = await executeListFiles({ path: relativePath, recursive: false }, config); // Verify all files are returned for (const [fileName, content] of createdFiles) { const fileEntry = result.files.find(f => f.name === fileName); expect(fileEntry, `File ${fileName} should be in listing`).toBeDefined(); expect(fileEntry?.type).toBe('file'); expect(fileEntry?.name).toBe(fileName); expect(fileEntry?.relativePath).toBe(fileName); expect(fileEntry?.size).toBeDefined(); expect(fileEntry?.lastModified).toBeDefined(); // Verify size matches content const expectedSize = Buffer.byteLength(content, 'utf-8'); expect(fileEntry?.size).toBe(expectedSize); // Verify lastModified is a valid ISO date expect(() => new Date(fileEntry!.lastModified!)).not.toThrow(); } // Verify all directories are returned for (const dirName of createdDirs) { const dirEntry = result.files.find(f => f.name === dirName); expect(dirEntry, `Directory ${dirName} should be in listing`).toBeDefined(); expect(dirEntry?.type).toBe('directory'); expect(dirEntry?.name).toBe(dirName); expect(dirEntry?.relativePath).toBe(dirName); expect(dirEntry?.lastModified).toBeDefined(); // Verify lastModified is a valid ISO date expect(() => new Date(dirEntry!.lastModified!)).not.toThrow(); } // Verify no extra entries const totalExpected = createdFiles.size + createdDirs.size; expect(result.files.length).toBe(totalExpected); }), { numRuns: 100 } ); }); /** * Property 3: Recursive listing completeness * For any directory in the workspace, listing with recursive=true should return * all nested files and directories at any depth within that directory. * * Feature: mcp-workspace-server, Property 3: Recursive listing completeness * Validates: Requirements 1.2 */ it('Property 3: should return all nested items when recursive=true', async () => { // Generator for nested directory structures const nestedStructureGenerator = fc.record({ // Root level files rootFiles: fc.uniqueArray( fc.record({ name: validNameGenerator().map(n => 'root-' + n + '.txt'), content: fileContentGenerator(), }), { minLength: 0, maxLength: 5, selector: (item) => item.name } ), // Nested directories with files nestedDirs: fc.array( fc.record({ dirPath: fc.array(validNameGenerator(), { minLength: 1, maxLength: 3 }) .map(parts => parts.join(path.sep)), files: fc.uniqueArray( fc.record({ name: validNameGenerator().map(n => n + '.txt'), content: fileContentGenerator(), }), { minLength: 0, maxLength: 3, selector: (item) => item.name } ), }), { minLength: 1, maxLength: 5 } ), }); await fc.assert( fc.asyncProperty(nestedStructureGenerator, async (structure) => { // Create a unique subdirectory for this test iteration const testSubdir = path.join(testWorkspace, `test-${Date.now()}-${Math.random().toString(36).substring(7)}`); await fs.mkdir(testSubdir, { recursive: true }); // Track all created files and directories const allFiles = new Map<string, string>(); // relativePath -> content const allDirs = new Set<string>(); // relativePath // Create root level files for (const file of structure.rootFiles) { const filePath = path.join(testSubdir, file.name); await fs.writeFile(filePath, file.content, 'utf-8'); allFiles.set(file.name, file.content); } // Create nested directories and files for (const nested of structure.nestedDirs) { const dirPath = path.join(testSubdir, nested.dirPath); await fs.mkdir(dirPath, { recursive: true }); // Track all parent directories let currentPath = ''; for (const part of nested.dirPath.split(path.sep)) { currentPath = currentPath ? path.join(currentPath, part) : part; allDirs.add(currentPath); } // Create files in this directory for (const file of nested.files) { const filePath = path.join(dirPath, file.name); const relativePath = path.join(nested.dirPath, file.name); await fs.writeFile(filePath, file.content, 'utf-8'); allFiles.set(relativePath, file.content); } } // List the directory recursively const relativePath = path.relative(testWorkspace, testSubdir); const result = await executeListFiles({ path: relativePath, recursive: true }, config); // Verify all files are returned for (const [fileRelPath, content] of allFiles) { const fileEntry = result.files.find(f => f.relativePath === fileRelPath); expect(fileEntry, `File ${fileRelPath} should be in recursive listing`).toBeDefined(); expect(fileEntry?.type).toBe('file'); expect(fileEntry?.size).toBeDefined(); // Verify size matches content const expectedSize = Buffer.byteLength(content, 'utf-8'); expect(fileEntry?.size).toBe(expectedSize); } // Verify all directories are returned for (const dirRelPath of allDirs) { const dirEntry = result.files.find(f => f.relativePath === dirRelPath); expect(dirEntry, `Directory ${dirRelPath} should be in recursive listing`).toBeDefined(); expect(dirEntry?.type).toBe('directory'); } // Verify we have at least the expected number of entries const expectedMinEntries = allFiles.size + allDirs.size; expect(result.files.length).toBeGreaterThanOrEqual(expectedMinEntries); }), { numRuns: 100 } ); }); });

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/ShayYeffet/mcp_server'

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