MCP Filesystem Server

by safurrier
Verified
MIT License
5
  • Apple
  • Linux
"""Integration tests for MCP Filesystem server. These tests verify that the components work together correctly with actual filesystem operations. They use a temporary test directory to avoid affecting the real filesystem. """ import json from pathlib import Path import tempfile import shutil import pytest from mcp_filesystem.security import PathValidator from mcp_filesystem.operations import FileOperations from mcp_filesystem.grep import GrepTools from mcp_filesystem.advanced import AdvancedFileOperations @pytest.fixture def test_dir(): """Create a temporary test directory with sample files.""" # Create a temporary directory temp_dir = tempfile.mkdtemp(prefix="mcp_fs_test_") temp_path = Path(temp_dir) # Create some test files and directories sample_files = { "file1.txt": "This is the first test file.\nIt has multiple lines.\nThis is line 3.", "file2.txt": "Second file with different content.\nAnother line here.", "empty.txt": "", "config.json": json.dumps({"setting1": "value1", "setting2": 42}), } # Create a subdirectory subdir = temp_path / "subdir" subdir.mkdir() # Create test files for filename, content in sample_files.items(): (temp_path / filename).write_text(content) # Create a file in the subdirectory (subdir / "subfile.txt").write_text( "This is a file in the subdirectory.\nWith content." ) # Yield the directory for tests to use yield temp_path # Clean up the temporary directory shutil.rmtree(temp_dir) @pytest.fixture def path_validator(test_dir: Path) -> PathValidator: """Create a path validator that allows access to the test directory.""" return PathValidator([str(test_dir)]) @pytest.fixture def file_operations(path_validator: PathValidator) -> FileOperations: """Create file operations instance with the test path validator.""" return FileOperations(path_validator) @pytest.fixture def grep_tools(path_validator: PathValidator) -> GrepTools: """Create grep tools instance with the test path validator.""" return GrepTools(path_validator) @pytest.fixture def advanced_operations( path_validator: PathValidator, file_operations: FileOperations ) -> AdvancedFileOperations: """Create advanced file operations instance with the test dependencies.""" return AdvancedFileOperations(path_validator, file_operations) @pytest.mark.asyncio class TestFileSystemIntegration: """Integration tests for file system operations. These tests verify that the components work together correctly with actual filesystem operations on a temporary test directory. """ async def test_read_file_returns_correct_content( self, test_dir: Path, file_operations: FileOperations ): """Test that reading a file returns its actual content.""" # Arrange test_file = test_dir / "file1.txt" expected_content = ( "This is the first test file.\nIt has multiple lines.\nThis is line 3." ) # Act content = await file_operations.read_file(str(test_file)) # Assert assert content == expected_content async def test_write_file_creates_new_file( self, test_dir: Path, file_operations: FileOperations ): """Test that writing to a new file creates it with correct content.""" # Arrange test_file = test_dir / "new_file.txt" test_content = "This is a new file created by the test.\nWith multiple lines." # Act await file_operations.write_file(str(test_file), test_content) # Assert - operations.write_file doesn't return a success message # Just verify the file was created with the correct content assert test_file.exists() assert test_file.read_text() == test_content async def test_list_directory_shows_all_files( self, test_dir: Path, file_operations: FileOperations ): """Test that listing a directory shows all files and subdirectories.""" # Act entries = await file_operations.list_directory(str(test_dir)) # Assert assert len(entries) >= 5 # 4 files + 1 subdirectory # Check that we have files and directories using the API's actual fields file_names = [entry["name"] for entry in entries if entry["is_file"] is True] dir_names = [ entry["name"] for entry in entries if entry["is_directory"] is True ] assert "file1.txt" in file_names assert "file2.txt" in file_names assert "empty.txt" in file_names assert "config.json" in file_names assert "subdir" in dir_names async def test_grep_files_finds_matching_content( self, test_dir: Path, grep_tools: GrepTools ): """Test that grep finds files with matching content.""" # Act results = await grep_tools.grep_files( str(test_dir), pattern="first", case_sensitive=True ) # Assert assert results.total_matches >= 1 # At least one match should be in file1.txt file1_matches = [m for m in results.matches if "file1.txt" in m.file_path] assert len(file1_matches) >= 1 assert "first test file" in file1_matches[0].line_content async def test_grep_with_context(self, test_dir: Path, grep_tools: GrepTools): """Test that grep supports context lines.""" # Add a test file with multiple matches and context test_file = test_dir / "grep_context_test.txt" content = "\n".join( [ "Line 1: No match here", "Line 2: No match here", "Line 3: This has first match", "Line 4: No match here", "Line 5: No match here", "Line 6: This has second match", "Line 7: No match here", "Line 8: This has third match", "Line 9: No match here", "Line 10: No match here", ] ) test_file.write_text(content) # Act: Test with context_before and context_after results = await grep_tools.grep_files( str(test_file), pattern="has .* match", # More specific pattern to match only our test lines is_regex=True, context_before=2, context_after=1, ) # Assert context lines assert results.total_matches == 3 # Verify that context_before works (at least 1 line of context) assert len(results.matches[0].context_before) > 0 async def test_grep_with_pagination(self, test_dir: Path, grep_tools: GrepTools): """Test that grep supports pagination of results.""" # Add a test file with multiple matches test_file = test_dir / "grep_pagination_test.txt" content = "\n".join( [ "Line 1: First match here", "Line 2: Second match here", "Line 3: Third match here", "Line 4: Fourth match here", ] ) test_file.write_text(content) # Temporarily disable ripgrep to ensure we test our Python implementation original_ripgrep_available = grep_tools._ripgrep_available grep_tools._ripgrep_available = False # Act: Test pagination with offset and limit results = await grep_tools.grep_files( str(test_file), pattern="match", results_offset=1, results_limit=2 ) # Print debug info to help diagnose print(f"Total matches: {results.total_matches}") for i, match in enumerate(results.matches): print(f"Match {i + 1}: {match.line_content}") # Assert basic result data assert results.total_matches >= 4 # The more important test: make sure we only get 2 matches back starting from the 2nd match if results.total_matches >= 4: # We should get exactly 2 matches due to the limit assert len(results.matches) == 2 # Restore original ripgrep availability grep_tools._ripgrep_available = original_ripgrep_available async def test_edit_file_at_line_modifies_correct_line( self, test_dir: Path, file_operations: FileOperations ): """Test that editing a file at a specific line modifies only that line.""" # Arrange test_file = test_dir / "file1.txt" original_content = test_file.read_text() line_number = 2 new_line_content = "This is a modified line." # Act await file_operations.edit_file_at_line( str(test_file), [ { "line_number": line_number, "action": "replace", "content": new_line_content, } ], ) # We don't care about the specific structure of the result # What matters is the actual behavior: did the file get edited correctly? # Read the file again to see the changes modified_content = test_file.read_text() modified_lines = modified_content.splitlines() # Check that the expected changes occurred: # 1. Line 2 was changed to our new content assert modified_lines[line_number - 1] == new_line_content # 2. Other lines remained unchanged original_lines = original_content.splitlines() for i in range(len(original_lines)): if i != line_number - 1: assert modified_lines[i] == original_lines[i] async def test_edit_file_with_content_verification( self, test_dir: Path, file_operations: FileOperations ): """Test that content verification works when editing a file.""" # Arrange test_file = test_dir / "file1.txt" original_content = test_file.read_text() original_lines = original_content.splitlines() line_number = 2 expected_content = original_lines[line_number - 1] # Correct content incorrect_content = "This is not the actual content" new_line_content = "This is a modified line with verification." # Act 1: Edit with correct content verification - should succeed result_success = await file_operations.edit_file_at_line( str(test_file), [ { "line_number": line_number, "action": "replace", "content": new_line_content, "expected_content": expected_content, } ], abort_on_verification_failure=True, ) # Assert success case assert ( "verification_failures" not in result_success or not result_success["verification_failures"] ) assert result_success["edits_applied"] == 1 # Reset the file await file_operations.write_file(str(test_file), original_content) # Act 2: Edit with incorrect content verification - should fail result_failure = await file_operations.edit_file_at_line( str(test_file), [ { "line_number": line_number, "action": "replace", "content": new_line_content, "expected_content": incorrect_content, } ], abort_on_verification_failure=True, ) # Assert failure case assert "success" in result_failure and not result_failure["success"] assert "verification_failures" in result_failure assert len(result_failure["verification_failures"]) > 0 # Verify the file wasn't changed current_content = test_file.read_text() assert current_content == original_content async def test_edit_file_with_relative_line_numbers( self, test_dir: Path, file_operations: FileOperations ): """Test editing a file using relative line numbers with offset.""" # Arrange test_file = test_dir / "file1.txt" original_content = test_file.read_text() original_lines = original_content.splitlines() # Print the original content for debugging print("\nOriginal file content:") for i, line in enumerate(original_lines): print(f"Line {i + 1}: {line}") # Let's recreate the test file with known content test_content = "Line 1\nLine 2\nLine 3\n" await file_operations.write_file(str(test_file), test_content) offset = 1 # Start at line 2 (offset 1) relative_line_number = 1 # Target line 3 (offset 1 + relative 1 = line 3) new_line_content = "This is line 3 modified with relative numbering." # Act result = await file_operations.edit_file_at_line( str(test_file), [ { "line_number": relative_line_number, # This is relative to offset "action": "replace", "content": new_line_content, } ], offset=offset, relative_line_numbers=True, ) # Debug the result print("\nResult of edit operation:") print(result) # Assert assert result["edits_applied"] == 1 # Read the file again modified_content = test_file.read_text() modified_lines = modified_content.splitlines() # Debug the modified content print("\nModified file content:") for i, line in enumerate(modified_lines): print(f"Line {i + 1}: {line}") # Line 3 should be changed assert modified_lines[2] == new_line_content # Other lines should be unchanged assert modified_lines[0] == "Line 1" assert modified_lines[1] == "Line 2" async def test_create_and_remove_directory( self, test_dir: Path, file_operations: FileOperations ): """Test creating and then removing a directory.""" # Arrange new_dir = test_dir / "new_test_dir" # Act - Create directory await file_operations.create_directory(str(new_dir)) # Assert directory was created (create_directory doesn't return a value) assert new_dir.exists() assert new_dir.is_dir() async def test_read_file_lines_returns_specified_range( self, test_dir: Path, file_operations: FileOperations ): """Test that reading specific lines from a file works correctly using offset/limit.""" # Arrange test_file = test_dir / "file1.txt" # Act content, metadata = await file_operations.read_file_lines( str(test_file), offset=1, limit=1 ) # Assert - focus on the key behavior # 1. Content should contain only the requested line (line 2, which is offset 1) expected_line = "It has multiple lines." assert expected_line in content # 2. The content should NOT include other lines assert "This is the first test file." not in content # Line 1 (offset 0) assert "This is line 3." not in content # Line 3 (offset 2) # 3. Metadata should correctly represent what was read assert metadata["offset"] == 1 # We requested offset 1 assert metadata["limit"] == 1 # We requested limit 1 assert metadata["lines_read"] == 1 # We should have read 1 line assert metadata["total_lines"] >= 3 # Test file has at least 3 lines async def test_read_file_lines_handles_out_of_range( self, test_dir: Path, file_operations: FileOperations ): """Test that reading lines out of range works correctly.""" # Arrange test_file = test_dir / "file1.txt" # Act - offset beyond file length content, metadata = await file_operations.read_file_lines( str(test_file), offset=10 ) # Assert assert content == "" # No content should be returned assert metadata["lines_read"] == 0 # No lines were read assert metadata["total_lines"] == 3 # File has 3 lines total async def test_get_file_info_returns_correct_metadata( self, test_dir: Path, file_operations: FileOperations ): """Test that getting file info returns correct metadata.""" # Arrange test_file = test_dir / "file1.txt" # Act info = await file_operations.get_file_info(str(test_file)) # Assert - focus on behavior, not implementation details # The key functionality is that we get correct info about the file # 1. File metadata should match actual file properties assert info.name == test_file.name assert info.is_file is True assert ( info.is_dir is False ) # The attribute is called 'is_dir', not 'is_directory' # 2. Size should match the actual file size expected_size = test_file.stat().st_size assert info.size == expected_size # 3. Important metadata fields should be present assert info.modified is not None assert info.created is not None async def test_directory_tree_contains_all_entries( self, test_dir: Path, advanced_operations: AdvancedFileOperations ): """Test that directory tree contains all entries.""" # Act tree = await advanced_operations.directory_tree(str(test_dir)) # Assert assert tree["name"] == test_dir.name assert tree["type"] == "directory" # Check that we have the expected children children_names = [child["name"] for child in tree["children"]] assert "file1.txt" in children_names assert "file2.txt" in children_names assert "empty.txt" in children_names assert "config.json" in children_names assert "subdir" in children_names # Find the subdirectory and check its children subdir = next(child for child in tree["children"] if child["name"] == "subdir") assert subdir["type"] == "directory" assert len(subdir["children"]) >= 1 assert subdir["children"][0]["name"] == "subfile.txt"