Skip to main content
Glama

File Patch MCP Server

by shenning00
test_apply.py11.5 kB
"""Tests for apply_patch tool. Tests the patch application functionality including: - Successful application - Context mismatch - Dry run mode (CRITICAL) - Empty patches - Edge cases - Security checks """ import pytest from pathlib import Path from patch_mcp.tools.apply import apply_patch class TestApplyPatch: """Test suite for apply_patch tool.""" def test_apply_success(self, tmp_path): """Apply patch successfully.""" # Create file file = tmp_path / "config.py" file.write_text("DEBUG = False\nLOG_LEVEL = 'INFO'\nPORT = 8000\n") # Create patch patch = """--- config.py +++ config.py @@ -1,3 +1,3 @@ DEBUG = False -LOG_LEVEL = 'INFO' +LOG_LEVEL = 'DEBUG' PORT = 8000 """ result = apply_patch(str(file), patch) # Verify success assert result["success"] is True assert result["file_path"] == str(file) assert result["applied"] is True assert "Successfully applied" in result["message"] # Verify changes assert result["changes"]["lines_added"] == 1 assert result["changes"]["lines_removed"] == 1 assert result["changes"]["hunks_applied"] == 1 # Verify file was actually modified content = file.read_text() assert "LOG_LEVEL = 'DEBUG'" in content assert "LOG_LEVEL = 'INFO'" not in content def test_apply_context_mismatch(self, tmp_path): """Context mismatch should fail gracefully.""" # Create file with different content file = tmp_path / "config.py" file.write_text("DEBUG = False\nLOG_LEVEL = 'WARNING'\nPORT = 8000\n") # Patch expects different content patch = """--- config.py +++ config.py @@ -1,3 +1,3 @@ DEBUG = False -LOG_LEVEL = 'INFO' +LOG_LEVEL = 'DEBUG' PORT = 8000 """ result = apply_patch(str(file), patch) # Should fail assert result["success"] is False assert result["applied"] is False assert result["error_type"] == "context_mismatch" # File should remain unchanged content = file.read_text() assert "LOG_LEVEL = 'WARNING'" in content def test_dry_run_success(self, tmp_path): """Dry run should validate without modifying.""" # Create file file = tmp_path / "config.py" original_content = "DEBUG = False\nLOG_LEVEL = 'INFO'\nPORT = 8000\n" file.write_text(original_content) # Create patch patch = """--- config.py +++ config.py @@ -1,3 +1,3 @@ DEBUG = False -LOG_LEVEL = 'INFO' +LOG_LEVEL = 'DEBUG' PORT = 8000 """ result = apply_patch(str(file), patch, dry_run=True) # Should succeed assert result["success"] is True assert result["applied"] is True assert "dry run" in result["message"] # Verify changes info assert result["changes"]["lines_added"] == 1 assert result["changes"]["lines_removed"] == 1 # CRITICAL: File should NOT be modified content = file.read_text() assert content == original_content assert "LOG_LEVEL = 'INFO'" in content assert "LOG_LEVEL = 'DEBUG'" not in content def test_dry_run_does_not_modify(self, tmp_path): """CRITICAL: Dry run must not modify file under any circumstance.""" # Create file file = tmp_path / "file.py" original = "line1\nline2\nline3\n" file.write_text(original) # Apply with dry run patch = """--- file.py +++ file.py @@ -1,3 +1,3 @@ line1 -line2 +modified line3 """ result = apply_patch(str(file), patch, dry_run=True) # Even though it succeeds, file should be unchanged assert result["success"] is True assert file.read_text() == original def test_empty_patch(self, tmp_path): """Empty patch should succeed with zero counts.""" file = tmp_path / "file.txt" original = "content\n" file.write_text(original) result = apply_patch(str(file), "") # Should succeed with no changes assert result["success"] is True assert result["changes"]["lines_added"] == 0 assert result["changes"]["lines_removed"] == 0 assert result["changes"]["hunks_applied"] == 0 # File should be unchanged assert file.read_text() == original def test_whitespace_only_changes(self, tmp_path): """Whitespace-only changes should be counted normally.""" file = tmp_path / "file.py" file.write_text("line1\nline2\nline3\n") # Add whitespace patch = """--- file.py +++ file.py @@ -1,3 +1,3 @@ line1 -line2 +line2 line3 """ result = apply_patch(str(file), patch) # Should succeed assert result["success"] is True assert result["changes"]["lines_added"] == 1 assert result["changes"]["lines_removed"] == 1 def test_reject_symlink(self, tmp_path): """Symlinks should be rejected.""" real_file = tmp_path / "real.txt" real_file.write_text("content\n") symlink = tmp_path / "link.txt" symlink.symlink_to(real_file) patch = """--- link.txt +++ link.txt @@ -1,1 +1,1 @@ -content +modified """ result = apply_patch(str(symlink), patch) assert result["success"] is False assert result["error_type"] == "symlink_error" assert result["applied"] is False def test_reject_binary(self, tmp_path): """Binary files should be rejected.""" binary = tmp_path / "binary.dat" binary.write_bytes(b"\x00\x01\x02" * 100) patch = """--- binary.dat +++ binary.dat @@ -1,1 +1,1 @@ -old +new """ result = apply_patch(str(binary), patch) assert result["success"] is False assert result["error_type"] == "binary_file" assert result["applied"] is False def test_apply_multiple_hunks(self, tmp_path): """Apply patch with multiple hunks.""" file = tmp_path / "file.py" file.write_text( "line1\nline2\nline3\nline4\nline5\n" "line6\nline7\nline8\nline9\nline10\n" ) patch = """--- file.py +++ file.py @@ -1,3 +1,3 @@ -line1 +line1 modified line2 line3 @@ -8,3 +8,3 @@ line8 -line9 +line9 modified line10 """ result = apply_patch(str(file), patch) # Should succeed assert result["success"] is True assert result["changes"]["hunks_applied"] == 2 assert result["changes"]["lines_added"] == 2 assert result["changes"]["lines_removed"] == 2 # Verify modifications content = file.read_text() assert "line1 modified" in content assert "line9 modified" in content def test_apply_addition_only(self, tmp_path): """Apply patch with only additions.""" file = tmp_path / "file.py" file.write_text("line1\nline2\n") patch = """--- file.py +++ file.py @@ -1,2 +1,3 @@ line1 +added line2 """ result = apply_patch(str(file), patch) assert result["success"] is True assert result["changes"]["lines_added"] == 1 assert result["changes"]["lines_removed"] == 0 # Verify addition content = file.read_text() assert "added" in content def test_apply_removal_only(self, tmp_path): """Apply patch with only removals.""" file = tmp_path / "file.py" file.write_text("line1\nremove_me\nline2\n") patch = """--- file.py +++ file.py @@ -1,3 +1,2 @@ line1 -remove_me line2 """ result = apply_patch(str(file), patch) assert result["success"] is True assert result["changes"]["lines_added"] == 0 assert result["changes"]["lines_removed"] == 1 # Verify removal content = file.read_text() assert "remove_me" not in content def test_apply_file_not_found(self, tmp_path): """Missing file should return error.""" missing = tmp_path / "missing.txt" patch = """--- missing.txt +++ missing.txt @@ -1,1 +1,1 @@ -old +new """ result = apply_patch(str(missing), patch) assert result["success"] is False assert result["error_type"] == "file_not_found" assert result["applied"] is False def test_dry_run_no_write_permission_needed(self, tmp_path): """Dry run should not require write permission.""" # Create read-only file file = tmp_path / "readonly.txt" file.write_text("content\n") file.chmod(0o444) patch = """--- readonly.txt +++ readonly.txt @@ -1,1 +1,1 @@ -content +modified """ # Dry run should succeed (no write check) result = apply_patch(str(file), patch, dry_run=True) assert result["success"] is True # Restore write permission for cleanup file.chmod(0o644) def test_apply_preserves_line_endings(self, tmp_path): """Apply should preserve line endings.""" file = tmp_path / "file.txt" file.write_text("line1\nline2\nline3\n") patch = """--- file.txt +++ file.txt @@ -1,3 +1,3 @@ line1 -line2 +modified line3 """ result = apply_patch(str(file), patch) assert result["success"] is True # Verify line endings preserved (all \n) content = file.read_text() assert "modified\n" in content def test_apply_creates_backup_atomically(self, tmp_path): """Apply should use atomic file replacement.""" file = tmp_path / "file.txt" original = "line1\nline2\nline3\n" file.write_text(original) patch = """--- file.txt +++ file.txt @@ -1,3 +1,3 @@ line1 -line2 +modified line3 """ result = apply_patch(str(file), patch) # Should succeed assert result["success"] is True # File should exist and be modified assert file.exists() assert "modified" in file.read_text() # No temp files should remain temp_files = list(tmp_path.glob(".patch_tmp_*")) assert len(temp_files) == 0 def test_dry_run_with_context_mismatch(self, tmp_path): """Dry run should fail on context mismatch too.""" file = tmp_path / "file.py" file.write_text("wrong\ncontent\n") patch = """--- file.py +++ file.py @@ -1,2 +1,2 @@ -expected +modified content """ result = apply_patch(str(file), patch, dry_run=True) # Should fail even in dry run assert result["success"] is False assert result["applied"] is False def test_apply_complex_changes(self, tmp_path): """Apply complex patch with additions, removals, and modifications.""" file = tmp_path / "complex.py" file.write_text("def foo():\n old_line1\n old_line2\n old_line3\n") patch = """--- complex.py +++ complex.py @@ -1,4 +1,5 @@ def foo(): - old_line1 - old_line2 + new_line1 + new_line2 + added_line old_line3 """ result = apply_patch(str(file), patch) assert result["success"] is True assert result["changes"]["lines_added"] == 3 assert result["changes"]["lines_removed"] == 2 # Verify result content = file.read_text() assert "new_line1" in content assert "new_line2" in content assert "added_line" in content assert "old_line1" not in content assert "old_line2" not in content assert "old_line3" in content

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/shenning00/patch_mcp'

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