Skip to main content
Glama
test_sync_health.py10.7 kB
""" Comprehensive tests for MCP file sync health and error handling. These tests catch the type of silent failures that occurred with advanced-memory-mcp where the watchdog failed to start. """ import pytest import tempfile import time from pathlib import Path from unittest.mock import Mock class MockSyncManager: """Mock sync manager for testing.""" def __init__(self, project_path: str): self.project_path = project_path self.sync_state = "INITIALIZING" self.files_scanned = 0 self.files_total = 0 self.errors = [] self.watcher = None self._start_time = time.time() def start_sync(self) -> bool: """Start synchronization.""" try: # Simulate counting files self.files_total = len(list(Path(self.project_path).rglob("*.md"))) self.sync_state = "SCANNING" return True except Exception as e: self.sync_state = "ERROR" self.errors.append(str(e)) return False def get_status(self) -> dict: """Get current sync status.""" return { "state": self.sync_state, "files_scanned": self.files_scanned, "files_total": self.files_total, "progress_percent": (self.files_scanned / self.files_total * 100) if self.files_total > 0 else 0, "errors": self.errors, "watcher_alive": self.watcher is not None if self.watcher else False, "runtime_seconds": time.time() - self._start_time, } @pytest.fixture def temp_project(): """Create temporary project with test markdown files.""" with tempfile.TemporaryDirectory() as tmpdir: project_path = Path(tmpdir) # Create test structure (project_path / "notes").mkdir() (project_path / "archive").mkdir() # Create test files for i in range(50): (project_path / "notes" / f"note_{i}.md").write_text( f"# Note {i}\n\nContent {i}" ) for i in range(30): (project_path / "archive" / f"old_{i}.md").write_text( f"# Old {i}\n\nArchived {i}" ) yield project_path @pytest.fixture def large_project(): """Create large project for performance testing.""" with tempfile.TemporaryDirectory() as tmpdir: project_path = Path(tmpdir) # Create 500 files for i in range(500): folder = project_path / f"folder_{i // 50}" folder.mkdir(exist_ok=True) (folder / f"file_{i}.md").write_text(f"# File {i}\n\n{'x' * 1000}") yield project_path class TestSyncInitialization: """Test sync initialization and startup.""" def test_sync_manager_initializes(self, temp_project): """Test that sync manager initializes properly.""" sync = MockSyncManager(str(temp_project)) assert sync.project_path == str(temp_project) assert sync.sync_state == "INITIALIZING" assert sync.files_scanned == 0 assert sync.files_total == 0 def test_sync_counts_files_on_start(self, temp_project): """Test that sync correctly counts files on startup.""" sync = MockSyncManager(str(temp_project)) result = sync.start_sync() assert result is True assert sync.files_total == 80 # 50 + 30 test files assert sync.sync_state == "SCANNING" def test_sync_handles_empty_directory(self): """Test sync with empty directory.""" with tempfile.TemporaryDirectory() as tmpdir: sync = MockSyncManager(tmpdir) result = sync.start_sync() assert result is True assert sync.files_total == 0 assert sync.sync_state == "SCANNING" def test_sync_handles_nonexistent_path(self): """Test sync with non-existent path.""" sync = MockSyncManager("/nonexistent/path/12345") result = sync.start_sync() assert result is False assert sync.sync_state == "ERROR" assert len(sync.errors) > 0 class TestSyncHealthChecks: """Test health check functionality.""" def test_get_status_returns_complete_info(self, temp_project): """Test that get_status returns all required fields.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() status = sync.get_status() assert "state" in status assert "files_scanned" in status assert "files_total" in status assert "progress_percent" in status assert "errors" in status assert "watcher_alive" in status assert "runtime_seconds" in status def test_status_shows_progress_percent(self, temp_project): """Test progress percentage calculation.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() # Simulate progress sync.files_scanned = 40 status = sync.get_status() assert status["progress_percent"] == 50.0 # 40/80 * 100 def test_status_handles_zero_files(self): """Test status with no files.""" with tempfile.TemporaryDirectory() as tmpdir: sync = MockSyncManager(tmpdir) sync.start_sync() status = sync.get_status() assert status["progress_percent"] == 0 class TestSyncStallDetection: """Test detection of stuck/stalled syncs.""" def test_detects_stalled_sync(self, temp_project): """Test detection when sync is stuck at same count.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() # Simulate stall initial_count = sync.files_scanned time.sleep(0.1) assert sync.files_scanned == initial_count status = sync.get_status() # In real implementation, would check for stall # Here we just verify state is accessible assert status["state"] == "SCANNING" def test_zero_progress_after_timeout_is_suspicious(self, temp_project): """Test that zero progress after timeout indicates problem.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() # Simulate time passing with no progress time.sleep(0.5) status = sync.get_status() if status["files_scanned"] == 0 and status["runtime_seconds"] > 0.1: # This would trigger alert in real implementation assert status["state"] == "SCANNING" assert status["runtime_seconds"] > 0 class TestSyncPerformance: """Test sync performance and scaling.""" @pytest.mark.timeout(10) def test_sync_completes_in_reasonable_time(self, temp_project): """Test that small sync completes quickly.""" sync = MockSyncManager(str(temp_project)) start = time.time() sync.start_sync() duration = time.time() - start # Counting 80 files should be instant assert duration < 1.0 @pytest.mark.slow @pytest.mark.timeout(30) def test_large_sync_scales_linearly(self, large_project): """Test that sync time scales reasonably with file count.""" sync = MockSyncManager(str(large_project)) start = time.time() sync.start_sync() duration = time.time() - start # 500 files should still be fast assert duration < 5.0 assert sync.files_total == 500 class TestSyncErrorHandling: """Test error handling in sync operations.""" def test_permission_error_is_caught(self, temp_project): """Test handling of permission errors.""" # Make directory read-only temp_project.chmod(0o444) sync = MockSyncManager(str(temp_project)) # This might not fail on all systems, but shouldn't crash try: sync.start_sync() # If it succeeds, that's also okay except PermissionError: # Expected on some systems pass finally: # Restore permissions temp_project.chmod(0o755) def test_errors_are_logged(self): """Test that errors are captured in error list.""" sync = MockSyncManager("/invalid/path") sync.start_sync() assert len(sync.errors) > 0 assert sync.sync_state == "ERROR" def test_watcher_death_is_detectable(self, temp_project): """Test that dead watcher is detectable.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() # Simulate watcher death sync.watcher = Mock() sync.watcher.is_alive = Mock(return_value=False) status = sync.get_status() # In real implementation, watcher_alive would check is_alive() assert "watcher_alive" in status class TestSyncRecovery: """Test sync recovery mechanisms.""" def test_sync_can_restart_after_error(self): """Test that sync can be restarted after failure.""" # First attempt fails sync = MockSyncManager("/nonexistent") result1 = sync.start_sync() assert result1 is False # Change to valid path and retry with tempfile.TemporaryDirectory() as tmpdir: sync.project_path = tmpdir sync.sync_state = "INITIALIZING" sync.errors = [] result2 = sync.start_sync() assert result2 is True class TestSyncMonitoring: """Test monitoring and observability.""" def test_runtime_is_tracked(self, temp_project): """Test that runtime is tracked accurately.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() time.sleep(0.2) status = sync.get_status() assert status["runtime_seconds"] >= 0.2 def test_progress_can_be_monitored(self, temp_project): """Test that progress can be monitored over time.""" sync = MockSyncManager(str(temp_project)) sync.start_sync() # Simulate progress progress_log = [] for i in range(10, 81, 10): sync.files_scanned = i status = sync.get_status() progress_log.append(status["progress_percent"]) # Progress should be increasing assert progress_log == sorted(progress_log) assert progress_log[-1] == 100.0 # Pytest configuration def pytest_configure(config): """Configure custom markers.""" config.addinivalue_line( "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" ) config.addinivalue_line("markers", "performance: marks tests as performance tests") if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])

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/sandraschi/notepadpp-mcp'

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