Skip to main content
Glama

File Patch MCP Server

by shenning00
test_error_scenarios.py21 kB
"""Integration tests for error scenarios and recovery patterns. This module tests all 10 error types in realistic scenarios, demonstrating how the recovery patterns handle various failure modes gracefully. """ import os import shutil import stat from pathlib import Path import pytest from patch_mcp.models import ErrorType from patch_mcp.recovery import ( batch_apply_patches, safe_apply_with_backup, safe_revert_with_validation, validate_before_apply, ) class TestErrorScenarios: """Test all error types in integration scenarios.""" def test_file_not_found_recovery(self, tmp_path): """Handle missing file gracefully.""" nonexistent = tmp_path / "nonexistent.py" patch = """--- nonexistent.py +++ nonexistent.py @@ -1,1 +1,1 @@ -old +new """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(nonexistent), patch) assert result["success"] is False assert result["applied"] is False assert "error" in result # Should fail at backup creation since file doesn't exist # Test validate_before_apply result = validate_before_apply(str(nonexistent), patch) assert result["success"] is False assert "error" in result def test_permission_denied_recovery(self, tmp_path): """Handle permission errors.""" readonly = tmp_path / "readonly.py" readonly.write_text("original content\n") # Make file read-only readonly.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) patch = """--- readonly.py +++ readonly.py @@ -1,1 +1,1 @@ -original content +modified content """ try: # Test safe_apply_with_backup result = safe_apply_with_backup(str(readonly), patch) # Should fail due to write permission assert result["success"] is False assert result["applied"] is False # File should still have original content content = readonly.read_text() assert content == "original content\n" finally: # Restore permissions for cleanup readonly.chmod(stat.S_IWUSR | stat.S_IRUSR) def test_invalid_patch_recovery(self, tmp_path): """Handle malformed patches.""" target = tmp_path / "target.py" target.write_text("some content\n") # Use a patch with proper format but invalid headers invalid_patch = """--- /dev/null +++ /dev/null This line breaks the format """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(target), invalid_patch) # May succeed or fail depending on patch_ng's tolerance # The important thing is it doesn't crash and file is safe assert "success" in result assert "applied" in result # Test validate_before_apply catches format issues result = validate_before_apply(str(target), invalid_patch) # Again, may succeed or fail, but should be handled safely assert "success" in result # File should be unchanged if operation failed if not result.get("applied", False): assert target.read_text() == "some content\n" def test_context_mismatch_recovery(self, tmp_path): """Handle mismatched context.""" target = tmp_path / "module.py" target.write_text("def foo():\n return 1\n") # Patch expects different content patch = """--- module.py +++ module.py @@ -1,2 +1,2 @@ def bar(): - return 2 + return 3 """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(target), patch) assert result["success"] is False assert result["applied"] is False assert "error" in result # Should detect context mismatch during validation # File should be unchanged assert target.read_text() == "def foo():\n return 1\n" def test_encoding_error_recovery(self, tmp_path): """Handle encoding issues.""" target = tmp_path / "binary.dat" # Write invalid UTF-8 bytes target.write_bytes(b"\xff\xfe Invalid UTF-8 \x80\x81") patch = """--- binary.dat +++ binary.dat @@ -1,1 +1,1 @@ -old +new """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(target), patch) # Should fail when trying to read the file assert result["success"] is False assert result["applied"] is False def test_io_error_recovery(self, tmp_path): """Handle I/O errors.""" # Test with a directory instead of a file directory = tmp_path / "subdir" directory.mkdir() patch = """--- subdir +++ subdir @@ -1,1 +1,1 @@ -old +new """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(directory), patch) assert result["success"] is False assert result["applied"] is False # Should fail when trying to backup/read directory def test_symlink_error_recovery(self, tmp_path): """Handle symlink rejection.""" target = tmp_path / "target.py" target.write_text("content\n") link = tmp_path / "link.py" link.symlink_to(target) patch = """--- link.py +++ link.py @@ -1,1 +1,1 @@ -content +modified """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(link), patch) assert result["success"] is False assert result["applied"] is False # Should be rejected due to symlink policy # Original file should be unchanged assert target.read_text() == "content\n" def test_binary_file_recovery(self, tmp_path): """Handle binary file rejection.""" binary = tmp_path / "image.png" # Write binary content (null bytes indicate binary) binary.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00") patch = """--- image.png +++ image.png @@ -1,1 +1,1 @@ -old +new """ # Test safe_apply_with_backup result = safe_apply_with_backup(str(binary), patch) assert result["success"] is False assert result["applied"] is False # Should be rejected as binary file def test_disk_space_error_recovery(self, tmp_path): """Handle insufficient disk space. Note: This is difficult to test reliably without actually filling the disk. We test that the validation logic exists and would trigger. """ target = tmp_path / "file.py" target.write_text("content\n") patch = """--- file.py +++ file.py @@ -1,1 +1,1 @@ -content +modified """ # We can't easily simulate out-of-disk-space in tests # but we verify the recovery pattern works result = safe_apply_with_backup(str(target), patch) # Should succeed in normal conditions assert result["success"] is True assert result["applied"] is True assert result["backup_file"] def test_resource_limit_recovery(self, tmp_path): """Handle resource limits (file size).""" large = tmp_path / "large.txt" # Create a file just under the limit size_mb = 9 # Just under 10MB limit large.write_bytes(b"x" * (size_mb * 1024 * 1024)) patch = """--- large.txt +++ large.txt @@ -1,1 +1,1 @@ x """ # Should work with file under limit result = safe_apply_with_backup(str(large), patch) # May fail or succeed depending on patch library handling of large files # The important thing is it doesn't crash assert "success" in result assert "applied" in result class TestRecoveryPatternIntegration: """Test recovery patterns in realistic scenarios.""" def test_safe_apply_with_backup_success(self, tmp_path): """Safe apply succeeds and creates backup.""" target = tmp_path / "config.py" target.write_text("DEBUG = False\n") patch = """--- config.py +++ config.py @@ -1,1 +1,1 @@ -DEBUG = False +DEBUG = True """ result = safe_apply_with_backup(str(target), patch) assert result["success"] is True assert result["applied"] is True assert result["restored"] is False assert result["backup_file"] assert Path(result["backup_file"]).exists() assert "changes" in result # Verify file was modified assert target.read_text() == "DEBUG = True\n" # Verify backup has original content backup_path = Path(result["backup_file"]) assert backup_path.read_text() == "DEBUG = False\n" def test_safe_apply_with_backup_failure_rollback(self, tmp_path): """Safe apply fails and rolls back automatically.""" target = tmp_path / "module.py" original_content = "line1\nline2\nline3\nline4\nline5\n" target.write_text(original_content) # Patch expects completely different content that won't match patch = """--- module.py +++ module.py @@ -1,5 +1,5 @@ different1 different2 -different3 +modified3 different4 different5 """ result = safe_apply_with_backup(str(target), patch) # Should fail due to context mismatch (or succeed with fuzzy matching) # Either way, we verify the recovery function handles it safely if not result["success"]: assert result["applied"] is False assert "error" in result # File should be unchanged if it failed assert target.read_text() == original_content else: # If it succeeded (fuzzy matching), that's also acceptable # The important thing is no data loss assert result["applied"] is True assert result["backup_file"] # Backup should have original content backup_path = Path(result["backup_file"]) assert backup_path.read_text() == original_content def test_validate_before_apply_dry_run(self, tmp_path): """Dry run validates without modifying file.""" target = tmp_path / "app.py" original = "x = 1\n" target.write_text(original) patch = """--- app.py +++ app.py @@ -1,1 +1,1 @@ -x = 1 +x = 2 """ result = validate_before_apply(str(target), patch, dry_run=True) assert result["success"] is True assert result["applied"] is False # Dry run doesn't apply assert "validation" in result assert result["validation"]["can_apply"] is True assert "inspection" in result # File should be unchanged assert target.read_text() == original def test_validate_before_apply_real_apply(self, tmp_path): """Validate and apply when not dry run.""" target = tmp_path / "utils.py" target.write_text("VERSION = '1.0'\n") patch = """--- utils.py +++ utils.py @@ -1,1 +1,1 @@ -VERSION = '1.0' +VERSION = '2.0' """ result = validate_before_apply(str(target), patch, dry_run=False) assert result["success"] is True assert result["applied"] is True assert "changes" in result assert "validation" in result assert "inspection" in result # File should be modified assert target.read_text() == "VERSION = '2.0'\n" def test_batch_apply_all_succeed(self, tmp_path): """All patches in batch apply successfully.""" file1 = tmp_path / "file1.py" file2 = tmp_path / "file2.py" file3 = tmp_path / "file3.py" file1.write_text("a = 1\n") file2.write_text("b = 2\n") file3.write_text("c = 3\n") patch1 = """--- file1.py +++ file1.py @@ -1,1 +1,1 @@ -a = 1 +a = 10 """ patch2 = """--- file2.py +++ file2.py @@ -1,1 +1,1 @@ -b = 2 +b = 20 """ patch3 = """--- file3.py +++ file3.py @@ -1,1 +1,1 @@ -c = 3 +c = 30 """ patches = [ (str(file1), patch1), (str(file2), patch2), (str(file3), patch3), ] result = batch_apply_patches(patches) assert result["success"] is True assert result["applied_count"] == 3 assert result["failed_count"] == 0 assert result["rollback_performed"] is False # All files should be modified assert file1.read_text() == "a = 10\n" assert file2.read_text() == "b = 20\n" assert file3.read_text() == "c = 30\n" def test_batch_apply_rollback_on_failure(self, tmp_path): """One failure rolls back all patches.""" file1 = tmp_path / "file1.py" file2 = tmp_path / "file2.py" file3 = tmp_path / "file3.py" original1 = "a = 1\n" original2 = "b = 2\n" original3 = "c = 3\n" file1.write_text(original1) file2.write_text(original2) file3.write_text(original3) patch1 = """--- file1.py +++ file1.py @@ -1,1 +1,1 @@ -a = 1 +a = 10 """ # This patch will fail (context mismatch) patch2 = """--- file2.py +++ file2.py @@ -1,1 +1,1 @@ -b = 999 +b = 20 """ patch3 = """--- file3.py +++ file3.py @@ -1,1 +1,1 @@ -c = 3 +c = 30 """ patches = [ (str(file1), patch1), (str(file2), patch2), # This will fail (str(file3), patch3), ] result = batch_apply_patches(patches) assert result["success"] is False assert result["applied_count"] == 1 # Only first patch applied before failure assert result["rollback_performed"] is True # All files should be rolled back to original assert file1.read_text() == original1 assert file2.read_text() == original2 assert file3.read_text() == original3 def test_safe_revert_success(self, tmp_path): """Safe revert succeeds when file unmodified.""" target = tmp_path / "service.py" original = "STATUS = 'active'\n" target.write_text(original) patch = """--- service.py +++ service.py @@ -1,1 +1,1 @@ -STATUS = 'active' +STATUS = 'inactive' """ # First apply the patch from patch_mcp.tools.apply import apply_patch apply_result = apply_patch(str(target), patch) assert apply_result["success"] is True assert target.read_text() == "STATUS = 'inactive'\n" # Now revert it result = safe_revert_with_validation(str(target), patch) assert result["success"] is True assert result["reverted"] is True assert result["backup_file"] assert "changes" in result # File should be back to original assert target.read_text() == original def test_safe_revert_detects_modification(self, tmp_path): """Safe revert fails if file was modified.""" target = tmp_path / "database.py" target.write_text("PORT = 5432\n") patch = """--- database.py +++ database.py @@ -1,1 +1,1 @@ -PORT = 5432 +PORT = 3306 """ # Apply patch from patch_mcp.tools.apply import apply_patch apply_result = apply_patch(str(target), patch) assert apply_result["success"] is True # Modify file further target.write_text("PORT = 8080\n") # Try to revert original patch result = safe_revert_with_validation(str(target), patch) assert result["success"] is False assert result["reverted"] is False assert "error" in result # Should fail because file was modified def test_safe_revert_creates_backup(self, tmp_path): """Safe revert creates backup before attempting.""" target = tmp_path / "auth.py" target.write_text("ENABLED = True\n") patch = """--- auth.py +++ auth.py @@ -1,1 +1,1 @@ -ENABLED = True +ENABLED = False """ # Apply patch from patch_mcp.tools.apply import apply_patch apply_result = apply_patch(str(target), patch) assert apply_result["success"] is True # Revert with backup result = safe_revert_with_validation(str(target), patch) assert result["success"] is True assert result["backup_file"] backup_path = Path(result["backup_file"]) assert backup_path.exists() # Backup should have the modified content (before revert) assert backup_path.read_text() == "ENABLED = False\n" # Target should have original content (after revert) assert target.read_text() == "ENABLED = True\n" class TestEndToEndWorkflows: """Test complete multi-step workflows.""" def test_generate_validate_apply_workflow(self, tmp_path): """Complete workflow: generate → validate → apply.""" from patch_mcp.tools.generate import generate_patch # Setup original and modified versions original = tmp_path / "original.py" modified = tmp_path / "modified.py" target = tmp_path / "target.py" original_content = "def add(a, b):\n return a + b\n" modified_content = "def add(a, b):\n return a + b + 1\n" original.write_text(original_content) modified.write_text(modified_content) target.write_text(original_content) # Step 1: Generate patch gen_result = generate_patch(str(original), str(modified)) assert gen_result["success"] is True patch = gen_result["patch"] # Step 2: Validate before applying val_result = validate_before_apply(str(target), patch, dry_run=True) assert val_result["success"] is True assert val_result["validation"]["can_apply"] is True # Step 3: Apply with safety apply_result = safe_apply_with_backup(str(target), patch) assert apply_result["success"] is True assert apply_result["applied"] is True # Verify result assert target.read_text() == modified_content def test_apply_modify_revert_workflow(self, tmp_path): """Apply patch, make changes, revert to original.""" target = tmp_path / "workflow.py" original = "step = 1\n" target.write_text(original) patch = """--- workflow.py +++ workflow.py @@ -1,1 +1,1 @@ -step = 1 +step = 2 """ # Step 1: Apply patch apply_result = safe_apply_with_backup(str(target), patch) assert apply_result["success"] is True assert target.read_text() == "step = 2\n" # Step 2: Revert to original revert_result = safe_revert_with_validation(str(target), patch) assert revert_result["success"] is True assert target.read_text() == original def test_backup_restore_multiple_versions(self, tmp_path): """Manage multiple backup versions.""" import time from patch_mcp.tools.backup import backup_file target = tmp_path / "versions.py" target.write_text("version = 1\n") # Create multiple backups at different stages backup1 = backup_file(str(target)) assert backup1["success"] is True # Sleep to ensure different timestamps time.sleep(1.1) target.write_text("version = 2\n") backup2 = backup_file(str(target)) assert backup2["success"] is True # Sleep to ensure different timestamps time.sleep(1.1) target.write_text("version = 3\n") backup3 = backup_file(str(target)) assert backup3["success"] is True # Verify we have 3 different backups assert backup1["backup_file"] != backup2["backup_file"] assert backup2["backup_file"] != backup3["backup_file"] # Verify each backup has correct content assert Path(backup1["backup_file"]).read_text() == "version = 1\n" assert Path(backup2["backup_file"]).read_text() == "version = 2\n" assert Path(backup3["backup_file"]).read_text() == "version = 3\n" def test_multi_file_patch_workflow(self, tmp_path): """Apply patch affecting multiple files.""" from patch_mcp.tools.inspect import inspect_patch file1 = tmp_path / "module1.py" file2 = tmp_path / "module2.py" file1.write_text("x = 1\n") file2.write_text("y = 2\n") # Create separate patches for each file (multi-file patches need to be split) patch1 = """--- module1.py +++ module1.py @@ -1,1 +1,1 @@ -x = 1 +x = 10 """ patch2 = """--- module2.py +++ module2.py @@ -1,1 +1,1 @@ -y = 2 +y = 20 """ # Multi-file patch for inspection multi_patch = patch1 + patch2 # Step 1: Inspect to see what files are affected inspect_result = inspect_patch(multi_patch) assert inspect_result["success"] is True assert len(inspect_result["files"]) == 2 # Step 2: Apply individual patches to each file patches = [(str(file1), patch1), (str(file2), patch2)] batch_result = batch_apply_patches(patches) assert batch_result["success"] is True # Verify both files modified assert file1.read_text() == "x = 10\n" assert file2.read_text() == "y = 20\n"

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