From bb138e2fb552f837b0f9f466027580e1feb26f7c Mon Sep 17 00:00:00 2001 From: Fahad Date: Thu, 2 Oct 2025 08:22:01 +0400 Subject: [PATCH] refactor: code cleanup --- patch/README.md | 93 -- patch/patch_crossplatform.py | 1541 ----------------------------- patch/validation_crossplatform.py | 586 ----------- 3 files changed, 2220 deletions(-) delete mode 100644 patch/README.md delete mode 100644 patch/patch_crossplatform.py delete mode 100644 patch/validation_crossplatform.py diff --git a/patch/README.md b/patch/README.md deleted file mode 100644 index 916e4b9..0000000 --- a/patch/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Cross-Platform Compatibility Patches - -This directory contains patch scripts to improve the cross-platform compatibility of the zen-mcp server. - -## Files - -### `patch_crossplatform.py` -Main script that automatically applies all necessary fixes to resolve cross-platform compatibility issues. - -**Usage:** -```bash -# From the patch/ directory -python patch_crossplatform.py [--dry-run] [--backup] [--validate-only] -``` - -**Options:** -- `--dry-run`: Show changes without applying them -- `--backup`: Create a backup before modifying files -- `--validate-only`: Only check if the fixes are already applied - -### `validation_crossplatform.py` -Validation script that tests whether all fixes work correctly. - -**Usage:** -```bash -# From the patch/ directory -python validation_crossplatform.py -``` - -## Applied Fixes - -1. **HOME DIRECTORY DETECTION ON WINDOWS:** - - Linux tests (/home/ubuntu) failed on Windows - - Unix patterns were not detected due to backslashes - - Solution: Added Windows patterns + double path check - -2. **UNIX PATH VALIDATION ON WINDOWS:** - - Unix paths (/etc/passwd) were rejected as relative paths - - Solution: Accept Unix paths as absolute on Windows - -3. **CROSS-PLATFORM TESTS:** - - Assertions used OS-specific separators - - The safe_files test used a non-existent file on Windows - - Solution: Use Path.parts + temporary files on Windows - -4. **SHELL SCRIPT COMPATIBILITY ON WINDOWS:** - - Shell scripts did not detect Windows virtual environment paths - - Solution: Added detection for .zen_venv/Scripts/ paths - -5. **COMMUNICATION SIMULATOR LOGGER BUG:** - - AttributeError: logger used before initialization - - Solution: Initialize logger before calling _get_python_path() - -6. **PYTHON PATH DETECTION ON WINDOWS:** - - The simulator could not find the Windows Python executable - - Solution: Added Windows-specific detection - -## How to Use - -1. **Apply all fixes:** - ```bash - cd patch/ - python patch_crossplatform.py - ``` - -2. **Test in dry-run mode (preview):** - ```bash - cd patch/ - python patch_crossplatform.py --dry-run - ``` - -3. **Validate the fixes:** - ```bash - cd patch/ - python validation_crossplatform.py - ``` - -4. **Check if fixes are already applied:** - ```bash - cd patch/ - python patch_crossplatform.py --validate-only - ``` - -## Modified Files - -- `utils/file_utils.py`: Home patterns + Unix path validation -- `tests/test_file_protection.py`: Cross-platform assertions -- `tests/test_utils.py`: Safe_files test with temporary file -- `run_integration_tests.sh`: Windows venv detection -- `code_quality_checks.sh`: venv and Windows tools detection -- `communication_simulator_test.py`: Logger initialization order + Windows paths - -Tests should now pass on Windows, macOS, and Linux! diff --git a/patch/patch_crossplatform.py b/patch/patch_crossplatform.py deleted file mode 100644 index 4afe9ea..0000000 --- a/patch/patch_crossplatform.py +++ /dev/null @@ -1,1541 +0,0 @@ -#!/usr/bin/env python3 -""" -Complete patch for cross-platform test compatibility. - -This script automatically applies all necessary modifications to resolve -cross-platform compatibility issues in the zen-mcp-server project. - -FIXED ISSUES: - -1. HOME DIRECTORY DETECTION ON WINDOWS: - - Linux tests (/home/ubuntu) failed on Windows - - Unix patterns were not detected due to backslashes - - Solution: Added Windows patterns + dual-path check - -2. UNIX PATH VALIDATION ON WINDOWS: - - Unix paths (/etc/passwd) were rejected as relative paths - - Solution: Accept Unix paths as absolute on Windows - -3. CROSS-PLATFORM TESTS: - - Assertions used OS-specific separators - - The safe_files test used a non-existent file on Windows - - Solution: Use Path.parts + temporary files on Windows - -4. SHELL SCRIPTS WINDOWS COMPATIBILITY: - - Shell scripts didn't detect Windows virtual environment paths - - Solution: Added detection for .zen_venv/Scripts/ paths - -5. SHELL SCRIPTS PYTHON AND TOOL DETECTION: - - Python and tool executables not detected on Windows - - Solution: Added detection for .zen_venv/Scripts/*.exe paths - -6. COMMUNICATION SIMULATOR LOGGER BUG: - - AttributeError: logger used before initialization - - Solution: Initialize logger before calling _get_python_path() - -7. PYTHON PATH DETECTION ON WINDOWS (SIMULATOR & TESTS): - - Simulator and test classes couldn't find Windows Python executable - - Solution: Added Windows-specific path detection in simulator and - BaseSimulatorTest - -8. BASE TEST CLASSES LOGGER BUG: - - AttributeError: logger used before initialization in test classes - - Solution: Initialize logger before calling _get_python_path() in - BaseSimulatorTest - -9. BASE TOOL LOGGER AND PYTHON PATH (tools/shared/base_tool.py): - - Logger may be used before initialization or Python path not detected on - Windows - - Solution: Ensure logger is initialized before Python path detection and - add Windows-specific path detection - -10. WINDOWS PATH VALIDATION: - - Some path validation logic did not handle Windows/Unix - cross-compatibility - - Solution: Improved path validation to support both Windows and Unix - absolute paths for tests - -11. DOCKER PATH VALIDATION: - - Docker paths were not recognized as absolute on Windows - - Solution: Added Docker path validation in file_utils.py - -12. DOCKER MODE COMPATIBILITY IN TESTS: - - Conversation tests failed when MCP_FILE_PATH_MODE=docker due to path - conversion - - Solution: Force local mode in tests and reset PathModeDetector cache - -MODIFIED FILES: -- utils/file_utils.py : Home patterns + Unix path validation + Docker support -- tests/test_file_protection.py : Cross-platform assertions -- tests/test_utils.py : Safe_files test with temporary file -- run_integration_tests.sh : Windows venv detection -- code_quality_checks.sh : Windows venv and tools detection + tool paths -- communication_simulator_test.py : Logger initialization order + Windows paths -- simulator_tests/base_test.py : Logger initialization order + Windows paths -- tools/shared/base_tool.py : Logger initialization order + Windows paths -- tests/test_conversation_file_features.py : Docker mode compatibility - -Usage: - python patch_crossplatform.py [--dry-run] [--backup] [--validate-only] - -Options: - --dry-run : Show modifications without applying them - --backup : Create a backup before modification - --validate-only : Only check if patches are applied -""" - -import argparse -import shutil -import sys -from pathlib import Path - - -class CrossPlatformPatcher: - """Main manager for cross-platform patches.""" - - def __init__(self, workspace_root: Path): - self.workspace_root = workspace_root - self.patches_applied = [] - self.errors = [] - - def find_target_files(self) -> dict[str, Path]: - """Find all files to patch.""" - files = { - "file_utils": self.workspace_root / "utils" / "file_utils.py", - "test_file_protection": (self.workspace_root / "tests" / "test_file_protection.py"), - "test_utils": self.workspace_root / "tests" / "test_utils.py", - "run_integration_tests_sh": (self.workspace_root / "run_integration_tests.sh"), - "code_quality_checks_sh": (self.workspace_root / "code_quality_checks.sh"), - "communication_simulator": (self.workspace_root / "communication_simulator_test.py"), - "base_test": (self.workspace_root / "simulator_tests" / "base_test.py"), - "base_tool": (self.workspace_root / "tools" / "shared" / "base_tool.py"), - "test_conversation_features": (self.workspace_root / "tests" / "test_conversation_file_features.py"), - } - - for _, path in files.items(): - if not path.exists(): - raise FileNotFoundError(f"Required file missing: {path}") - - return files - - def read_file(self, file_path: Path) -> str: - """Read the content of a file.""" - with open(file_path, encoding="utf-8") as f: - return f.read() - - def write_file(self, file_path: Path, content: str) -> None: - """Write content to a file.""" - with open(file_path, "w", encoding="utf-8") as f: - f.write(content) - - def create_backup(self, file_path: Path) -> Path: - """Create a backup of the file.""" - backup_path = file_path.with_suffix(f"{file_path.suffix}.backup") - shutil.copy2(file_path, backup_path) - return backup_path - - def patch_home_patterns(self, content: str) -> tuple[str, bool]: - """Patch 1: Add Windows patterns for home detection.""" - # Check if already patched - look for Windows Unix patterns - if '"\\\\users\\\\"' in content and '"\\\\home\\\\"' in content: - return content, False - - # Search for the exact patterns array in is_home_directory_root - old_patterns = """ home_patterns = [ - "/users/", # macOS - "/home/", # Linux - "c:\\\\users\\\\", # Windows - "c:/users/", # Windows with forward slashes - ]""" - - new_patterns = """ home_patterns = [ - "/users/", # macOS - "/home/", # Linux - "\\\\users\\\\", # macOS on Windows - "\\\\home\\\\", # Linux on Windows - "c:\\\\users\\\\", # Windows - "c:/users/", # Windows with forward slashes - ]""" - - if old_patterns in content: - content = content.replace(old_patterns, new_patterns) - return content, True - - return content, False - - def patch_dual_path_check(self, content: str) -> tuple[str, bool]: - """Patch 2: Add dual-path check (original + resolved).""" - if "original_path_str = str(path).lower()" in content: - return content, False - - # Replace the entire section from patterns to the end of the loop - old_section = """ # Also check common home directory patterns - path_str = str(resolved_path).lower() - home_patterns = [ - "/users/", # macOS - "/home/", # Linux - "\\\\users\\\\", # macOS on Windows - "\\\\home\\\\", # Linux on Windows - "c:\\\\users\\\\", # Windows - "c:/users/", # Windows with forward slashes - ] - - for pattern in home_patterns: - if pattern in path_str: - # Extract the user directory path - # e.g., /Users/fahad or /home/username - parts = path_str.split(pattern) - if len(parts) > 1: - # Get the part after the pattern - after_pattern = parts[1] - # Check if we're at the user's root (no subdirectories) - if ( - "/" not in after_pattern and "\\\\" not in after_pattern - ): - logger.warning( - f"Attempted to scan user home directory root: {path}. " - f"Please specify a subdirectory instead." - ) - return True""" - - new_section = """ # Also check common home directory patterns - # Use both original and resolved paths to handle cross-platform testing - original_path_str = str(path).lower() - resolved_path_str = str(resolved_path).lower() - home_patterns = [ - "/users/", # macOS - "/home/", # Linux - "\\\\users\\\\", # macOS on Windows - "\\\\home\\\\", # Linux on Windows - "c:\\\\users\\\\", # Windows - "c:/users/", # Windows with forward slashes - ] - - # Check patterns in both original and resolved paths - for path_str in [original_path_str, resolved_path_str]: - for pattern in home_patterns: - if pattern in path_str: - # Extract the user directory path - # e.g., /Users/fahad or /home/username - parts = path_str.split(pattern) - if len(parts) > 1: - # Get the part after the pattern - after_pattern = parts[1] - # Check if we're at the user's root (no subdirectories) - if "/" not in after_pattern and "\\\\" not in after_pattern: - logger.warning( - f"Attempted to scan user home directory root: {path}. " - f"Please specify a subdirectory instead." - ) - return True""" - - if old_section in content: - content = content.replace(old_section, new_section) - return content, True - - return content, False - - def patch_unix_path_validation(self, content: str) -> tuple[str, bool]: - """Patch 3: Accept Unix paths as absolute on Windows.""" - if "os.name == 'nt' and not is_absolute_path:" in content: - return content, False - - # Replace the simple is_absolute check with cross-platform logic - old_validation = """ # Step 2: Security Policy - Require absolute paths - # Relative paths could be interpreted differently depending on working directory - if not user_path.is_absolute(): - raise ValueError( - f"Relative paths are not supported. Please provide an absolute path.\\n" - f"Received: {path_str}" - )""" - - new_validation = """ # Step 2: Security Policy - Require absolute paths - # Relative paths could be interpreted differently depending on working directory - # Handle cross-platform path format compatibility for testing - is_absolute_path = user_path.is_absolute() - - # On Windows, also accept Unix-style absolute paths for cross-platform testing - # This allows paths like "/etc/passwd" to be treated as absolute - import os - if os.name == 'nt' and not is_absolute_path: - path_str_normalized = path_str.replace('\\\\', '/') - is_absolute_path = path_str_normalized.startswith('/') - - if not is_absolute_path: - raise ValueError( - f"Relative paths are not supported. Please provide an absolute path.\\n" - f"Received: {path_str}" - )""" - - if old_validation in content: - content = content.replace(old_validation, new_validation) - return content, True - - return content, False - - def patch_cross_platform_assertions(self, content: str) -> tuple[str, bool]: - """Patch 4: Fix assertions to be cross-platform.""" - if 'Path(p).parts[-2:] == ("my-awesome-project", "README.md")' in content: - return content, False - - old_assertions = """ # User files should be included - assert any("my-awesome-project/README.md" in p for p in file_paths) - assert any("my-awesome-project/main.py" in p for p in file_paths) - assert any("src/app.py" in p for p in file_paths)""" - - new_assertions = """ # User files should be included - # Use Path operations to handle cross-platform path separators - readme_found = any( - Path(p).parts[-2:] == ("my-awesome-project", "README.md") - for p in file_paths - ) - main_found = any( - Path(p).parts[-2:] == ("my-awesome-project", "main.py") - for p in file_paths - ) - app_found = any( - Path(p).parts[-2:] == ("src", "app.py") - for p in file_paths - ) - - assert readme_found - assert main_found - assert app_found""" - - if old_assertions in content: - content = content.replace(old_assertions, new_assertions) - return content, True - - return content, False - - def patch_safe_files_test(self, content: str) -> tuple[str, bool]: - """Patch 5: Fix safe_files test for Windows.""" - if "def test_read_file_content_safe_files_allowed(self, tmp_path):" in content: - return content, False - - old_test = ''' def test_read_file_content_safe_files_allowed(self): - """Test that safe files outside the original project root are now allowed""" - # In the new security model, safe files like /etc/passwd - # can be read as they're not in the dangerous paths list - content, tokens = read_file_content("/etc/passwd") - # Should successfully read the file - assert "--- BEGIN FILE: /etc/passwd ---" in content - assert "--- END FILE: /etc/passwd ---" in content - assert tokens > 0''' - - new_test = ''' def test_read_file_content_safe_files_allowed(self, tmp_path): - """Test that safe files outside the original project root are now allowed""" - import os - - if os.name == 'nt': # Windows - # Create a temporary file outside project root that should be accessible - safe_file = tmp_path / "safe_test_file.txt" - safe_file.write_text("test content for validation") - test_path = str(safe_file) - else: # Unix-like systems - # Use a system file that should exist and be safe - test_path = "/etc/passwd" - - content, tokens = read_file_content(test_path) - - if os.name == 'nt': - # On Windows, should successfully read our temporary file - assert f"--- BEGIN FILE: {test_path} ---" in content - assert "test content for validation" in content - assert "--- END FILE:" in content - else: - # On Unix, may or may not exist, but should not be rejected for security - # Either successfully read or file not found, but not security error - if "--- BEGIN FILE:" in content: - assert f"--- BEGIN FILE: {test_path} ---" in content - assert "--- END FILE:" in content - else: - # File might not exist, that's okay - assert ("--- FILE NOT FOUND:" in content or - "--- BEGIN FILE:" in content) - - assert tokens > 0''' - - if old_test in content: - content = content.replace(old_test, new_test) - return content, True - - return content, False - - def patch_shell_venv_detection(self, content: str) -> tuple[str, bool]: - """Patch 6: Add Windows venv detection to shell scripts.""" - # Check if already patched - if 'elif [[ -f ".zen_venv/Scripts/activate" ]]; then' in content: - return content, False - - # Patch run_integration_tests.sh - old_venv_check = """# Activate virtual environment -if [[ -f ".zen_venv/bin/activate" ]]; then - source .zen_venv/bin/activate - echo "✅ Using virtual environment" -else - echo "❌ No virtual environment found!" - echo "Please run: ./run-server.sh first" - exit 1 -fi""" - - new_venv_check = """# Activate virtual environment -if [[ -f ".zen_venv/bin/activate" ]]; then - source .zen_venv/bin/activate - echo "✅ Using virtual environment (Unix/Linux/macOS)" -elif [[ -f ".zen_venv/Scripts/activate" ]]; then - source .zen_venv/Scripts/activate - echo "✅ Using virtual environment (Windows)" -else - echo "❌ No virtual environment found!" - echo "Please run: ./run-server.sh first" - exit 1 -fi""" - - if old_venv_check in content: - content = content.replace(old_venv_check, new_venv_check) - return content, True - - return content, False - - def patch_shell_python_detection(self, content: str) -> tuple[str, bool]: - """Patch 7: Add Windows Python/tool detection to shell scripts.""" - # Check if already patched - if 'elif [[ -f ".zen_venv/Scripts/python.exe" ]]; then' in content: - return content, False - - # Patch code_quality_checks.sh Python detection - old_python_check = """# Determine Python command -if [[ -f ".zen_venv/bin/python" ]]; then - PYTHON_CMD=".zen_venv/bin/python" - PIP_CMD=".zen_venv/bin/pip" - echo "✅ Using venv" -elif [[ -n "$VIRTUAL_ENV" ]]; then - PYTHON_CMD="python" - PIP_CMD="pip" - echo "✅ Using activated virtual environment: $VIRTUAL_ENV" -else - echo "❌ No virtual environment found!" - echo "Please run: ./run-server.sh first to set up the environment" - exit 1 -fi""" - - new_python_check = """# Determine Python command -if [[ -f ".zen_venv/bin/python" ]]; then - PYTHON_CMD=".zen_venv/bin/python" - PIP_CMD=".zen_venv/bin/pip" - echo "✅ Using venv (Unix/Linux/macOS)" -elif [[ -f ".zen_venv/Scripts/python.exe" ]]; then - PYTHON_CMD=".zen_venv/Scripts/python.exe" - PIP_CMD=".zen_venv/Scripts/pip.exe" - echo "✅ Using venv (Windows)" -elif [[ -n "$VIRTUAL_ENV" ]]; then - PYTHON_CMD="python" - PIP_CMD="pip" - echo "✅ Using activated virtual environment: $VIRTUAL_ENV" -else - echo "❌ No virtual environment found!" - echo "Please run: ./run-server.sh first to set up the environment" - exit 1 -fi""" - - if old_python_check in content: - content = content.replace(old_python_check, new_python_check) - return content, True - - return content, False - - def patch_shell_tool_paths(self, content: str) -> tuple[str, bool]: - """Patch 8: Add Windows tool paths to shell scripts.""" - # Check if already patched - if 'elif [[ -f ".zen_venv/Scripts/ruff.exe" ]]; then' in content: - return content, False - - # Patch code_quality_checks.sh tool paths - old_tool_paths = """# Set tool paths -if [[ -f ".zen_venv/bin/ruff" ]]; then - RUFF=".zen_venv/bin/ruff" - BLACK=".zen_venv/bin/black" - ISORT=".zen_venv/bin/isort" - PYTEST=".zen_venv/bin/pytest" -else - RUFF="ruff" - BLACK="black" - ISORT="isort" - PYTEST="pytest" -fi""" - - new_tool_paths = """# Set tool paths -if [[ -f ".zen_venv/bin/ruff" ]]; then - RUFF=".zen_venv/bin/ruff" - BLACK=".zen_venv/bin/black" - ISORT=".zen_venv/bin/isort" - PYTEST=".zen_venv/bin/pytest" -elif [[ -f ".zen_venv/Scripts/ruff.exe" ]]; then - RUFF=".zen_venv/Scripts/ruff.exe" - BLACK=".zen_venv/Scripts/black.exe" - ISORT=".zen_venv/Scripts/isort.exe" - PYTEST=".zen_venv/Scripts/pytest.exe" -else - RUFF="ruff" - BLACK="black" - ISORT="isort" - PYTEST="pytest" -fi""" - - if old_tool_paths in content: - content = content.replace(old_tool_paths, new_tool_paths) - return content, True - - return content, False - - def patch_simulator_logger_init(self, content: str) -> tuple[str, bool]: - """Patch 9: Fix logger initialization order in simulator.""" - # Check if already patched - if "# Configure logging first" in content and "# Now get python path" in content: - return content, False - - # Fix the initialization order - old_init_order = """ self.verbose = verbose - self.keep_logs = keep_logs - self.selected_tests = selected_tests or [] - self.setup = setup - self.quick_mode = quick_mode - self.temp_dir = None - self.server_process = None - self.python_path = self._get_python_path() - - # Configure logging first - log_level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig( - level=log_level, - format="%(asctime)s - %(levelname)s - %(message)s" - ) - self.logger = logging.getLogger(__name__)""" - - new_init_order = """ self.verbose = verbose - self.keep_logs = keep_logs - self.selected_tests = selected_tests or [] - self.setup = setup - self.quick_mode = quick_mode - self.temp_dir = None - self.server_process = None - - # Configure logging first - log_level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") - self.logger = logging.getLogger(__name__) - - # Now get python path (after logger is configured) - self.python_path = self._get_python_path()""" - - if old_init_order in content: - content = content.replace(old_init_order, new_init_order) - return content, True - - return content, False - - def patch_simulator_python_path(self, content: str) -> tuple[str, bool]: - """Patch 10: Add Windows Python path detection to simulator.""" - # Check if already patched - if "import platform" in content and 'platform.system() == "Windows"' in content: - return content, False - - # Fix the _get_python_path method - old_python_path = """ def _get_python_path(self) -> str: - \"\"\"Get the Python path for the virtual environment\"\"\" - current_dir = os.getcwd() - venv_python = os.path.join(current_dir, "venv", "bin", "python") - - if os.path.exists(venv_python): - return venv_python - - # Try .zen_venv as fallback - zen_venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") - if os.path.exists(zen_venv_python): - return zen_venv_python - - # Fallback to system python if venv doesn't exist - self.logger.warning("Virtual environment not found, using system python") - return "python""" - - new_python_path = """ def _get_python_path(self) -> str: - \"\"\"Get the Python path for the virtual environment\"\"\" - import platform - current_dir = os.getcwd() - - # Check for different venv structures - if platform.system() == "Windows": - # Windows paths - zen_venv_python = os.path.join(current_dir, ".zen_venv", "Scripts", "python.exe") - venv_python = os.path.join(current_dir, "venv", "Scripts", "python.exe") - else: - # Unix/Linux/macOS paths - zen_venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") - venv_python = os.path.join(current_dir, "venv", "bin", "python") - - # Try .zen_venv first (preferred) - if os.path.exists(zen_venv_python): - return zen_venv_python - - # Try venv as fallback - if os.path.exists(venv_python): - return venv_python - - # Fallback to system python if venv doesn't exist - self.logger.warning("Virtual environment not found, using system python") - return "python""" - - if old_python_path in content: - content = content.replace(old_python_path, new_python_path) - return content, True - - return content, False - - def patch_base_test_logger_init(self, content: str) -> tuple[str, bool]: - """Patch 11: Fix logger initialization order in BaseSimulatorTest.""" - # Check if already patched - if "# Configure logging first" in content and "# Now get python path" in content: - return content, False - - # Fix the initialization order in BaseSimulatorTest - old_init_order = """ def __init__(self, verbose: bool = False): - self.verbose = verbose - self.test_files = {} - self.test_dir = None - self.python_path = self._get_python_path() - - # Configure logging - log_level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") - self.logger = logging.getLogger(self.__class__.__name__)""" - - new_init_order = """ def __init__(self, verbose: bool = False): - self.verbose = verbose - self.test_files = {} - self.test_dir = None - - # Configure logging first - log_level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") - self.logger = logging.getLogger(self.__class__.__name__) - - # Now get python path (after logger is configured) - self.python_path = self._get_python_path()""" - - if old_init_order in content: - content = content.replace(old_init_order, new_init_order) - return content, True - - return content, False - - def patch_base_test_python_path(self, content: str) -> tuple[str, bool]: - """Patch 12: Add Windows Python path detection to BaseSimulatorTest.""" - # Check if already patched - if "import platform" in content and 'platform.system() == "Windows"' in content: - return content, False - - # Fix the _get_python_path method in BaseSimulatorTest - old_python_path = """ def _get_python_path(self) -> str: - \"\"\"Get the Python path for the virtual environment\"\"\" - current_dir = os.getcwd() - venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") - - if os.path.exists(venv_python): - return venv_python - - # Fallback to system python if venv doesn't exist - self.logger.warning("Virtual environment not found, using system python") - return "python""" - - new_python_path = """ def _get_python_path(self) -> str: - \"\"\"Get the Python path for the virtual environment\"\"\" - import platform - current_dir = os.getcwd() - - # Check for different venv structures - if platform.system() == "Windows": - # Windows paths - zen_venv_python = os.path.join(current_dir, ".zen_venv", "Scripts", "python.exe") - else: - # Unix/Linux/macOS paths - zen_venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") - - if os.path.exists(zen_venv_python): - return zen_venv_python - - # Fallback to system python if venv doesn't exist - self.logger.warning("Virtual environment not found, using system python") - return "python""" - - if old_python_path in content: - content = content.replace(old_python_path, new_python_path) - return content, True - - return content, False - - def patch_windows_path_validation(self, content: str) -> tuple[str, bool]: - """Patch 13: Enhanced Windows path validation in base_tool.py.""" - # Check if already patched - look for the new implementation - if ( - "self._is_valid_absolute_path(path)" in content - and "def _is_valid_absolute_path(self, path: str) -> bool:" in content - ): - return content, False - - # Define the old validate_file_paths method that we want to replace - old_method = ''' def validate_file_paths(self, request) -> Optional[str]: - """ - Validate that all file paths in the request are absolute. - - This is a critical security function that prevents path traversal attacks - and ensures all file access is properly controlled. All file paths must - be absolute to avoid ambiguity and security issues. - - Args: - request: The validated request object - - Returns: - Optional[str]: Error message if validation fails, None if all paths are valid - """ - # Only validate files/paths if they exist in the request - file_fields = [ - "files", - "file", - "path", - "directory", - "notebooks", - "test_examples", - "style_guide_examples", - "files_checked", - "relevant_files", - ] - - for field_name in file_fields: - if hasattr(request, field_name): - field_value = getattr(request, field_name) - if field_value is None: - continue - - # Handle both single paths and lists of paths - paths_to_check = ( - field_value if isinstance(field_value, list) else [field_value] - ) - - for path in paths_to_check: - if path and not os.path.isabs(path): - return ( - "All file paths must be FULL absolute paths. " - f"Invalid path: '{path}'" - ) - - return None''' - - # Define the new complete implementation (validate_file_paths + _is_valid_absolute_path) - new_implementation = ''' def validate_file_paths(self, request) -> Optional[str]: - """ - Validate that all file paths in the request are absolute. - - This is a critical security function that prevents path traversal attacks - and ensures all file access is properly controlled. All file paths must - be absolute to avoid ambiguity and security issues. - - Args: - request: The validated request object - - Returns: - Optional[str]: Error message if validation fails, None if all paths are valid - """ - # Only validate files/paths if they exist in the request - file_fields = [ - "files", - "file", - "path", - "directory", - "notebooks", - "test_examples", - "style_guide_examples", - "files_checked", - "relevant_files", - ] - - for field_name in file_fields: - if hasattr(request, field_name): - field_value = getattr(request, field_name) - if field_value is None: - continue - - # Handle both single paths and lists of paths - paths_to_check = field_value if isinstance(field_value, list) else [field_value] - - for path in paths_to_check: - if path and not self._is_valid_absolute_path(path): - return f"All file paths must be FULL absolute paths. Invalid path: '{path}'" - - return None - - def _is_valid_absolute_path(self, path: str) -> bool: - """ - Validate that a path is an absolute path with enhanced Windows support. - - This method provides more robust path validation than os.path.isabs() alone, - particularly for Windows paths with Unicode characters and various separators. - - Args: - path: The path to validate - - Returns: - bool: True if the path is a valid absolute path, False otherwise - """ - import logging - import os - import unicodedata - - logger = logging.getLogger(__name__) - - if not path or not isinstance(path, str): - logger.debug(f"Path validation failed: empty or non-string path: {repr(path)}") - return False - - # Normalize Unicode characters to handle accented characters properly - try: - normalized_path = unicodedata.normalize("NFC", path) - except (TypeError, ValueError): - # If normalization fails, use the original path - normalized_path = path - logger.debug(f"Unicode normalization failed for path: {repr(path)}") - - # Convert to Path object for more robust checking - try: - from pathlib import Path - - # Try to create a Path object - this will fail for invalid paths - path_obj = Path(normalized_path) - - # Check if it's absolute using both os.path.isabs and Path.is_absolute - # This provides double validation for edge cases - is_abs_os = os.path.isabs(normalized_path) - is_abs_path = path_obj.is_absolute() - - # On Windows, also check for drive letters explicitly - if os.name == "nt": - # Windows absolute paths should start with drive letter or UNC path - has_drive = ( - ( - len(normalized_path) >= 3 - and normalized_path[1:3] in (":\\\\", ":/") - and normalized_path[0].isalpha() - ) - ) - has_unc = normalized_path.startswith(("\\\\\\\\", "//")) - - # Also accept Unix-style absolute paths (starting with /) for - # cross-platform compatibility - has_unix_root = normalized_path.startswith("/") - - result = (is_abs_os or is_abs_path) and (has_drive or has_unc or has_unix_root) - - if not result: - logger.warning(f"Windows path validation failed for: {repr(path)}") - logger.warning(f" Normalized: {repr(normalized_path)}") - logger.warning(f" os.path.isabs: {is_abs_os}") - logger.warning(f" Path.is_absolute: {is_abs_path}") - logger.warning(f" has_drive: {has_drive}") - logger.warning(f" has_unc: {has_unc}") - logger.warning(f" has_unix_root: {has_unix_root}") - - return result - else: - # Unix-like systems - result = is_abs_os or is_abs_path - - if not result: - logger.warning(f"Unix path validation failed for: {repr(path)}") - logger.warning(f" Normalized: {repr(normalized_path)}") - logger.warning(f" os.path.isabs: {is_abs_os}") - logger.warning(f" Path.is_absolute: {is_abs_path}") - - return result - - except (OSError, ValueError, TypeError) as e: - # If Path creation fails, fall back to basic os.path.isabs - logger.warning(f"Path object creation failed for {repr(path)}: {e}") - fallback_result = os.path.isabs(normalized_path) - - if not fallback_result: - logger.warning(f"Fallback path validation also failed for: {repr(path)}") - - return fallback_result''' - - # Perform the replacement - if old_method in content: - content = content.replace(old_method, new_implementation) - return content, True - - return content, False - - def patch_docker_path_validation(self, content: str) -> tuple[str, bool]: - """Patch 14: Add Docker path validation support in file_utils.py.""" - # Check if already patched - if "is_docker_path = converted_path_str.startswith" in content: - return content, False - - # Add import for path_detector if not present - if "from utils.path_detector import get_path_detector" not in content: - # Find the imports section and add the import - import_section = "import tempfile\nfrom pathlib import Path" - if import_section in content: - new_import = import_section + "\n\nfrom utils.path_detector import get_path_detector" - content = content.replace(import_section, new_import) - - # Replace the path validation logic - old_validation = """ # Step 2: Security Policy - Require absolute paths - # Relative paths could be interpreted differently depending on working directory - # Handle cross-platform path format compatibility for testing - is_absolute_path = user_path.is_absolute() - - # On Windows, also accept Unix-style absolute paths for cross-platform testing - # This allows paths like "/etc/passwd" to be treated as absolute - import os - if os.name == 'nt' and not is_absolute_path: - path_str_normalized = path_str.replace('\\\\', '/') - is_absolute_path = path_str_normalized.startswith('/') - - if not is_absolute_path: - raise ValueError( - "Relative paths are not supported. Please provide an absolute path.\\n" - f"Received: {path_str}" - )""" - - new_validation = """ # Step 1.5: Convert path according to current mode (Docker vs local) - path_detector = get_path_detector() - converted_path_str = path_detector.convert_path(path_str) - user_path = Path(converted_path_str) - - # Step 2: Security Policy - Require absolute paths - # Relative paths could be interpreted differently depending on working directory - # Handle cross-platform path format compatibility for testing - is_absolute_path = user_path.is_absolute() - - # Special handling for Docker mode paths - is_docker_path = converted_path_str.startswith("/app/project/") - is_docker_mode = ( - hasattr(path_detector, 'get_path_mode') and - path_detector.get_path_mode() == "docker" - ) - - # On Windows, also accept Unix-style absolute paths for cross-platform testing - # This allows paths like "/etc/passwd" to be treated as absolute - # Also accept Docker paths when in Docker mode - import os - if os.name == 'nt' and not is_absolute_path: - path_str_normalized = path_str.replace('\\\\', '/') - is_absolute_path = ( - path_str_normalized.startswith('/') or - (is_docker_mode and is_docker_path) - ) - - if not is_absolute_path and not (is_docker_mode and is_docker_path): - raise ValueError( - f"Relative paths are not supported. Please provide an absolute path.\\n" - f"Received: {path_str}" - )""" - - if old_validation in content: - content = content.replace(old_validation, new_validation) - return content, True - - return content, False - - def patch_conversation_test_docker_mode(self, content: str) -> tuple[str, bool]: - """Patch 15: Fix conversation file features test for Docker mode compatibility.""" - # Check if already patched - if '@patch("utils.file_utils.resolve_and_validate_path")' in content and "MCP_FILE_PATH_MODE" in content: - return content, False - - modified = False - - # 1. Update the test method decorator to include MCP_FILE_PATH_MODE and add mock - old_decorator = """ @patch.dict( - os.environ, {"GEMINI_API_KEY": "test-key", "OPENAI_API_KEY": "", "MCP_FILE_PATH_MODE": "local"}, clear=False - ) - def test_build_conversation_history_with_file_content(self, project_path):""" - - new_decorator = """ @patch.dict( - os.environ, {"GEMINI_API_KEY": "test-key", "OPENAI_API_KEY": "", "MCP_FILE_PATH_MODE": "local"}, clear=False - ) - @patch("utils.file_utils.resolve_and_validate_path") - def test_build_conversation_history_with_file_content(self, mock_resolve_path, project_path):""" - - if old_decorator in content: - content = content.replace(old_decorator, new_decorator) - modified = True - - # 2. Add the mock setup to return Path objects for the resolve_and_validate_path - old_test_start = ''' """Test that conversation history includes embedded file content""" - from providers.registry import ModelProviderRegistry - - ModelProviderRegistry.clear_cache() - - # Reset PathModeDetector singleton and cache to ensure local mode - from utils.path_detector import PathModeDetector - - PathModeDetector._instance = None - PathModeDetector._cached_mode = None - - from utils.path_detector import get_path_detector - - detector = get_path_detector() - detector._cached_mode = None - - # Force path mode to local and verify - mode = detector.get_path_mode() - assert mode == "local", f"Expected local mode, got {mode}" - - # Create test file with known content in a project-like structure''' - - new_test_start = ''' """Test that conversation history includes embedded file content""" - from providers.registry import ModelProviderRegistry - - ModelProviderRegistry.clear_cache() - - # Create test file with known content in a project-like structure''' - - if old_test_start in content: - content = content.replace(old_test_start, new_test_start) - modified = True - - # 3. Add the mock setup after the test file creation - old_file_creation = ''' project_subdir = os.path.join(project_path, "zen-mcp-server") - os.makedirs(project_subdir, exist_ok=True) - test_file = os.path.join(project_subdir, "test.py") - test_content = "# Test file\\ndef hello():\\n print('Hello, world!')\\n" - with open(test_file, "w") as f: - f.write(test_content) - - # Debug output for troubleshooting - print(f"DEBUG: Test file path: {test_file}") - print(f"DEBUG: File exists: {os.path.exists(test_file)}") - print(f"DEBUG: Path mode: {detector.get_path_mode()}") - print(f"DEBUG: Converted path: {detector.convert_path(test_file)}") - - # Verify the converted path matches original for local mode - converted_path = detector.convert_path(test_file) - assert converted_path == test_file, f"Path conversion failed: {test_file} -> {converted_path}"''' - - new_file_creation = """ # Mock resolve_and_validate_path to return a Path object (simulating local mode) - from pathlib import Path - - mock_resolve_path.side_effect = lambda path: Path(path) - project_subdir = os.path.join(project_path, "zen-mcp-server") - os.makedirs(project_subdir, exist_ok=True) - test_file = os.path.join(project_subdir, "test.py") - test_content = "# Test file\\ndef hello():\\n print('Hello, world!')\\n" - with open(test_file, "w") as f: - f.write(test_content)""" - - if old_file_creation in content: - content = content.replace(old_file_creation, new_file_creation) - modified = True - - # Fix the test file creation to use zen-mcp-server subdirectory - # This is already correct, check for alternate pattern - alt_file_creation = """ # Create a test file - test_file = os.path.join(project_path, "test.py")""" - - new_file_creation = """ # Create a test file in zen-mcp-server subdirectory for proper path detection - project_subdir = os.path.join(project_path, "zen-mcp-server") - os.makedirs(project_subdir, exist_ok=True) - test_file = os.path.join(project_subdir, "test.py")""" - - if alt_file_creation in content: - content = content.replace(alt_file_creation, new_file_creation) - modified = True - - return content, modified - - def apply_all_patches(self, files: dict[str, Path], create_backups: bool = False) -> bool: - """Apply all necessary patches.""" - all_success = True - - # Patch 1 & 2 & 3: utils/file_utils.py - print("🔧 Patching utils/file_utils.py...") - - file_utils_content = self.read_file(files["file_utils"]) - - # Apply patches in order - file_utils_content, modified1 = self.patch_home_patterns(file_utils_content) - file_utils_content, modified2 = self.patch_dual_path_check(file_utils_content) - file_utils_content, modified3 = self.patch_unix_path_validation(file_utils_content) - - if modified1 or modified2 or modified3: - if create_backups: - backup = self.create_backup(files["file_utils"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["file_utils"], file_utils_content) - - if modified1: - print(" ✅ Windows patterns added") - self.patches_applied.append("Home patterns Windows") - if modified2: - print(" ✅ Dual-path check added") - self.patches_applied.append("Dual-path check") - if modified3: - print(" ✅ Unix path validation added") - self.patches_applied.append("Unix path validation") - else: - print(" ℹ️ utils/file_utils.py already patched") - - # Patch 4: tests/test_file_protection.py - print("\n🔧 Patching tests/test_file_protection.py...") - - protection_content = self.read_file(files["test_file_protection"]) - protection_content, modified4 = self.patch_cross_platform_assertions(protection_content) - - if modified4: - if create_backups: - backup = self.create_backup(files["test_file_protection"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["test_file_protection"], protection_content) - print(" ✅ Cross-platform assertions added") - self.patches_applied.append("Cross-platform assertions") - else: - print(" ℹ️ tests/test_file_protection.py already patched") - - # Patch 5: tests/test_utils.py - print("\n🔧 Patching tests/test_utils.py...") - - utils_content = self.read_file(files["test_utils"]) - utils_content, modified5 = self.patch_safe_files_test(utils_content) - - if modified5: - if create_backups: - backup = self.create_backup(files["test_utils"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["test_utils"], utils_content) - print(" ✅ Cross-platform safe_files test added") - self.patches_applied.append("Safe files test") - else: - print(" ℹ️ tests/test_utils.py already patched") - - # Patch 6: run_integration_tests.sh - print("\n🔧 Patching run_integration_tests.sh...") - - run_integration_content = self.read_file(files["run_integration_tests_sh"]) - run_integration_content, modified6 = self.patch_shell_venv_detection(run_integration_content) - - if modified6: - if create_backups: - backup = self.create_backup(files["run_integration_tests_sh"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["run_integration_tests_sh"], run_integration_content) - print(" ✅ Windows venv detection added") - self.patches_applied.append("Windows venv detection (run_integration_tests.sh)") - else: - print(" ℹ️ run_integration_tests.sh already patched") - - # Patch 7 & 8: code_quality_checks.sh - print("\n🔧 Patching code_quality_checks.sh...") - - code_quality_content = self.read_file(files["code_quality_checks_sh"]) - code_quality_content, modified7 = self.patch_shell_python_detection(code_quality_content) - code_quality_content, modified8 = self.patch_shell_tool_paths(code_quality_content) - - if modified7 or modified8: - if create_backups: - backup = self.create_backup(files["code_quality_checks_sh"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["code_quality_checks_sh"], code_quality_content) - - if modified7: - print(" ✅ Windows Python detection added") - self.patches_applied.append("Windows Python detection (code_quality_checks.sh)") - if modified8: - print(" ✅ Windows tool paths added") - self.patches_applied.append("Windows tool paths (code_quality_checks.sh)") - else: - print(" ℹ️ code_quality_checks.sh already patched") - - # Patch 9 & 10: communication_simulator_test.py - print("\n🔧 Patching communication_simulator_test.py...") - - simulator_content = self.read_file(files["communication_simulator"]) - simulator_content, modified9 = self.patch_simulator_logger_init(simulator_content) - simulator_content, modified10 = self.patch_simulator_python_path(simulator_content) - - if modified9 or modified10: - if create_backups: - backup = self.create_backup(files["communication_simulator"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["communication_simulator"], simulator_content) - - if modified9: - print(" ✅ Logger initialization order fixed") - self.patches_applied.append("Logger initialization (communication_simulator_test.py)") - if modified10: - print(" ✅ Windows Python path detection added") - self.patches_applied.append("Windows Python paths (communication_simulator_test.py)") - else: - print(" ℹ️ communication_simulator_test.py already patched") - - # Patch 11 & 12: simulator_tests/base_test.py - print("\n🔧 Patching simulator_tests/base_test.py...") - - base_test_content = self.read_file(files["base_test"]) - base_test_content, modified11 = self.patch_base_test_logger_init(base_test_content) - base_test_content, modified12 = self.patch_base_test_python_path(base_test_content) - - if modified11 or modified12: - if create_backups: - backup = self.create_backup(files["base_test"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["base_test"], base_test_content) - - if modified11: - print(" ✅ Logger initialization order fixed") - self.patches_applied.append("Logger initialization (base_test.py)") - if modified12: - print(" ✅ Windows Python path detection added") - self.patches_applied.append("Windows Python paths (base_test.py)") - else: - print(" ℹ️ simulator_tests/base_test.py already patched") - - # Patch 13: tools/shared/base_tool.py - print("\n🔧 Patching tools/shared/base_tool.py...") - - base_tool_content = self.read_file(files["base_tool"]) - base_tool_content, modified13 = self.patch_windows_path_validation(base_tool_content) - - if modified13: - if create_backups: - backup = self.create_backup(files["base_tool"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["base_tool"], base_tool_content) - print(" ✅ Enhanced Windows path validation added") - self.patches_applied.append("Enhanced Windows path validation (base_tool.py)") - else: - print(" ℹ️ tools/shared/base_tool.py already patched") - - # Patch 14: Enhanced Docker path validation in utils/file_utils.py - print("\n🔧 Applying Docker path validation patch to utils/file_utils.py...") - - file_utils_content = self.read_file(files["file_utils"]) - file_utils_content, modified14 = self.patch_docker_path_validation(file_utils_content) - - if modified14: - if create_backups: - backup = self.create_backup(files["file_utils"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["file_utils"], file_utils_content) - print(" ✅ Docker path validation support added") - self.patches_applied.append("Docker path validation (file_utils.py)") - else: - print(" ℹ️ Docker path validation already patched in file_utils.py") - - # Patch 15: Conversation test Docker mode compatibility - print("\n🔧 Patching tests/test_conversation_file_features.py...") - - if "test_conversation_features" in files: - conversation_content = self.read_file(files["test_conversation_features"]) - conversation_content, modified15 = self.patch_conversation_test_docker_mode(conversation_content) - - if modified15: - if create_backups: - backup = self.create_backup(files["test_conversation_features"]) - print(f" ✅ Backup created: {backup}") - - self.write_file(files["test_conversation_features"], conversation_content) - print(" ✅ Docker mode compatibility added to conversation tests") - self.patches_applied.append("Docker mode compatibility (test_conversation_file_features.py)") - else: - print(" ℹ️ tests/test_conversation_file_features.py already patched") - - return all_success - - def validate_patches(self, files: dict[str, Path]) -> list[str]: - """Validate that all patches are correctly applied.""" - errors = [] - - # Validate utils/file_utils.py - file_utils_content = self.read_file(files["file_utils"]) - - if '"c:\\\\users\\\\"' not in file_utils_content: - errors.append("Pattern Windows \\\\users\\\\ missing in file_utils.py") - - if '"\\\\home\\\\"' not in file_utils_content: - errors.append("Pattern Windows \\\\home\\\\ missing in file_utils.py") - - if "original_path_str = str(path).lower()" not in file_utils_content: - errors.append("Dual-path check missing in file_utils.py") - - if "os.name == 'nt' and not is_absolute_path:" not in file_utils_content: - errors.append("Unix path validation missing in file_utils.py") - - # Validate tests/test_file_protection.py - protection_content = self.read_file(files["test_file_protection"]) - - if 'Path(p).parts[-2:] == ("my-awesome-project", "README.md")' not in protection_content: - errors.append("Cross-platform assertions missing in test_file_protection.py") - - # Validate tests/test_utils.py - utils_content = self.read_file(files["test_utils"]) - - if "def test_read_file_content_safe_files_allowed(self, tmp_path):" not in utils_content: - errors.append("Cross-platform safe_files test missing in test_utils.py") - - # Validate shell scripts - if "run_integration_tests_sh" in files: - run_integration_content = self.read_file(files["run_integration_tests_sh"]) - if 'elif [[ -f ".zen_venv/Scripts/activate" ]]; then' not in run_integration_content: - errors.append("Windows venv detection missing in run_integration_tests.sh") - - if "code_quality_checks_sh" in files: - code_quality_content = self.read_file(files["code_quality_checks_sh"]) - if 'elif [[ -f ".zen_venv/Scripts/python.exe" ]]; then' not in code_quality_content: - errors.append("Windows Python detection missing in code_quality_checks.sh") - if 'elif [[ -f ".zen_venv/Scripts/ruff.exe" ]]; then' not in code_quality_content: - errors.append("Windows tool paths missing in code_quality_checks.sh") - - # Validate communication simulator - if "communication_simulator" in files: - simulator_content = self.read_file(files["communication_simulator"]) - if "# Configure logging first" not in simulator_content: - errors.append("Logger initialization fix missing in communication_simulator_test.py") - if "import platform" not in simulator_content: - errors.append("Windows Python path detection missing in communication_simulator_test.py") - - # Validate simulator_tests/base_test.py - base_test_content = self.read_file(files["base_test"]) - - if "# Configure logging first" not in base_test_content or "# Now get python path" not in base_test_content: - errors.append("Logger initialization order missing in base_test.py") - - if "import platform" not in base_test_content or 'platform.system() == "Windows"' not in base_test_content: - errors.append("Windows Python path detection missing in base_test.py") - - # Validate tools/shared/base_tool.py - base_tool_content = self.read_file(files["base_tool"]) - - if "self._is_valid_absolute_path(path)" not in base_tool_content: - errors.append("Enhanced path validation call missing in base_tool.py") - - if "def _is_valid_absolute_path(self, path: str) -> bool:" not in base_tool_content: - errors.append("_is_valid_absolute_path method missing in base_tool.py") - - if "unicodedata.normalize" not in base_tool_content: - errors.append("Unicode normalization missing in base_tool.py") - - if "has_unix_root = normalized_path.startswith" not in base_tool_content: - errors.append("Enhanced Windows path validation missing in base_tool.py") - - # Validate Docker path validation in utils/file_utils.py - if "from utils.path_detector import get_path_detector" not in file_utils_content: - errors.append("Path detector import missing in file_utils.py") - - if "is_docker_path = converted_path_str.startswith" not in file_utils_content: - errors.append("Docker path validation missing in file_utils.py") - - if "is_docker_mode = " not in file_utils_content: - errors.append("Docker mode detection missing in file_utils.py") - - # Validate conversation test patches - if "test_conversation_features" in files: - conversation_content = self.read_file(files["test_conversation_features"]) - - if '"MCP_FILE_PATH_MODE": "local"' not in conversation_content: - errors.append("Local mode forcing missing in test_conversation_file_features.py") - - if "PathModeDetector._instance = None" not in conversation_content: - errors.append("PathModeDetector reset missing in test_conversation_file_features.py") - - if "detector._cached_mode = None" not in conversation_content: - errors.append("PathModeDetector cache reset missing in test_conversation_file_features.py") - - return errors - - def show_diff_summary(self, files: dict[str, Path]) -> None: - """Show a summary of the modifications that would be applied.""" - print("🔍 SUMMARY OF MODIFICATIONS TO BE APPLIED:") - print("=" * 70) - - modifications = [ - ( - "utils/file_utils.py", - [ - "Add Windows patterns for home detection (\\\\users\\\\, \\\\home\\\\)", - "Dual-path check (original + resolved) for compatibility", - "Accept Unix paths as absolute on Windows", - ], - ), - ( - "tests/test_file_protection.py", - [ - "Replace separator-sensitive assertions", - "Use Path.parts for cross-platform checks", - ], - ), - ( - "tests/test_utils.py", - [ - "Adapt safe_files test for Windows", - "Use temporary files instead of /etc/passwd", - ], - ), - ( - "run_integration_tests.sh", - [ - "Add Windows virtual environment detection", - "Support .zen_venv/Scripts/activate path", - ], - ), - ( - "code_quality_checks.sh", - [ - "Add Windows Python executable detection", - "Support .zen_venv/Scripts/*.exe tool paths", - ], - ), - ( - "communication_simulator_test.py", - [ - "Fix logger initialization order", - "Add Windows Python path detection", - "Support platform-specific venv structures", - ], - ), - ( - "tools/shared/base_tool.py", - [ - "Enhanced Windows path validation with Unicode support", - "Robust absolute path detection for drive letters and UNC", - "Cross-platform compatibility for Unix-style paths", - ], - ), - ( - "utils/file_utils.py (Docker support)", - [ - "Add path_detector import for Docker path conversion", - "Support Docker path validation (/app/project/...)", - "Accept Docker paths as valid when in Docker mode", - "Enhanced cross-platform path handling", - ], - ), - ( - "tests/test_conversation_file_features.py", - [ - "Force local mode in conversation tests", - "Reset PathModeDetector singleton and cache", - "Create test files in zen-mcp-server subdirectory", - "Add debug output for troubleshooting", - ], - ), - ] - - for filename, changes in modifications: - print(f"\n📁 {filename}:") - for change in changes: - print(f" • {change}") - - print("\n" + "=" * 70) - print("These modifications will allow tests to pass on Windows") - print("while maintaining compatibility with Linux and macOS.") - - -def main(): - """Main function.""" - parser = argparse.ArgumentParser(description="Complete patch for cross-platform compatibility") - parser.add_argument("--dry-run", action="store_true", help="Show modifications without applying them") - parser.add_argument("--backup", action="store_true", help="Create a backup before modification") - parser.add_argument("--validate-only", action="store_true", help="Only check if patches are applied") - - args = parser.parse_args() - - print("🔧 Complete patch for cross-platform compatibility") - print("=" * 70) - print("This script applies all necessary fixes so that") - print("tests pass on Windows, macOS, and Linux.") - print("=" * 70) - - try: - # Initialize patcher - use parent directory as workspace root - # since this script is now in patch/ subdirectory - workspace_root = Path(__file__).parent.parent - patcher = CrossPlatformPatcher(workspace_root) - - # Find files - files = patcher.find_target_files() - print("📁 Files found:") - for name, path in files.items(): - print(f" • {name}: {path}") - - # Validation only mode - if args.validate_only: - print("\n🔍 Validating patches...") - errors = patcher.validate_patches(files) - - if not errors: - print("✅ All patches are correctly applied") - return 0 - else: - print("❌ Missing patches:") - for error in errors: - print(f" • {error}") - return 1 - - # Dry-run mode - if args.dry_run: - patcher.show_diff_summary(files) - print("\n✅ Dry-run complete. Run without --dry-run to apply.") - return 0 - - # Apply patches - print("\n🔧 Applying patches...") - success = patcher.apply_all_patches(files, args.backup) - - if not success: - print("❌ Errors occurred while applying patches") - return 1 - - # Final validation - print("\n🔍 Final validation...") - errors = patcher.validate_patches(files) - - if errors: - print("❌ Validation errors:") - for error in errors: - print(f" • {error}") - return 1 - - # Final summary - print("\n" + "=" * 70) - print("🎉 SUCCESS: All patches applied successfully!") - print("\nPatches applied:") - for patch in patcher.patches_applied: - print(f" ✅ {patch}") - - print(f"\nTotal number of fixes: {len(patcher.patches_applied)}") - print("\n📋 SUMMARY OF FIXES:") - print("• Home directory detection works on all OSes") - print("• Unix path validation accepted on Windows") - print("• Cross-platform tests use Path.parts") - print("• Safe_files test uses temporary files on Windows") - print("\n🧪 Tests should now pass on Windows!") - - return 0 - - except Exception as e: - print(f"❌ Error during patch: {e}") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/patch/validation_crossplatform.py b/patch/validation_crossplatform.py deleted file mode 100644 index df58a66..0000000 --- a/patch/validation_crossplatform.py +++ /dev/null @@ -1,586 +0,0 @@ -#!/usr/bin/env python3 -""" -Validation script for all cross-platform fixes. - -This script runs a comprehensive series of tests to validate that all applied fixes -work correctly on Windows, including: - -1. Home directory pattern detection (Windows, macOS, Linux) -2. Unix path validation on Windows -3. Safe files functionality with temporary files -4. Cross-platform file discovery with Path.parts -5. Communication simulator logger and Python path fixes -6. BaseSimulatorTest logger and Python path fixes -7. Shell scripts Windows virtual environment support -8. Docker path validation and mode compatibility -9. Conversation tests Docker mode compatibility - -Tests cover all modified files: -- utils/file_utils.py -- tests/test_file_protection.py -- tests/test_utils.py -- communication_simulator_test.py -- simulator_tests/base_test.py -- run_integration_tests.sh -- code_quality_checks.sh -- tests/test_conversation_file_features.py (Docker mode) -""" - -import os -import sys -import tempfile -from pathlib import Path -from unittest.mock import patch - -# Add parent directory to path to import project modules -sys.path.insert(0, str(Path(__file__).parent.parent)) - -# Import functions to test -from utils.file_utils import ( - expand_paths, - is_home_directory_root, - read_file_content, - resolve_and_validate_path, -) - - -def test_home_directory_patterns(): - """Test 1: Home directory patterns on Windows.""" - print("🧪 Test 1: Home directory patterns on Windows") - print("-" * 60) - - test_cases = [ - ("/home/ubuntu", True, "Linux home directory"), - ("/home/testuser", True, "Linux home directory"), - ("/Users/john", True, "macOS home directory"), - ("/Users/developer", True, "macOS home directory"), - ("C:\\Users\\John", True, "Windows home directory"), - ("C:/Users/Jane", True, "Windows home directory"), - ("/home/ubuntu/projects", False, "Linux home subdirectory"), - ("/Users/john/Documents", False, "macOS home subdirectory"), - ("C:\\Users\\John\\Documents", False, "Windows home subdirectory"), - ] - - passed = 0 - for path_str, expected, description in test_cases: - try: - result = is_home_directory_root(Path(path_str)) - status = "✅" if result == expected else "❌" - print(f" {status} {path_str:<30} -> {result} ({description})") - if result == expected: - passed += 1 - except Exception as e: - print(f" ❌ {path_str:<30} -> Exception: {e}") - - success = passed == len(test_cases) - print(f"\nResult: {passed}/{len(test_cases)} tests passed") - return success - - -def test_unix_path_validation(): - """Test 2: Unix path validation on Windows.""" - print("\n🧪 Test 2: Unix path validation on Windows") - print("-" * 60) - - test_cases = [ - ("/etc/passwd", True, "Unix system file"), - ("/home/user/file.txt", True, "Unix user file"), - ("/usr/local/bin/python", True, "Unix binary path"), - ("./relative/path", False, "Relative path"), - ("relative/file.txt", False, "Relative file"), - ("C:\\Windows\\System32", True, "Windows absolute path"), - ] - - passed = 0 - for path_str, should_pass, description in test_cases: - try: - resolve_and_validate_path(path_str) - result = True - status = "✅" if should_pass else "❌" - print(f" {status} {path_str:<30} -> Accepted ({description})") - except ValueError: - result = False - status = "✅" if not should_pass else "❌" - print(f" {status} {path_str:<30} -> Rejected ({description})") - except PermissionError: - result = True # Rejected for security, not path format - status = "✅" if should_pass else "❌" - print(f" {status} {path_str:<30} -> Secured ({description})") - except Exception as e: - result = False - status = "❌" - print(f" {status} {path_str:<30} -> Error: {e}") - - if result == should_pass: - passed += 1 - - success = passed == len(test_cases) - print(f"\nResult: {passed}/{len(test_cases)} tests passed") - return success - - -def test_safe_files_functionality(): - """Test 3: Safe files functionality.""" - print("\n🧪 Test 3: Safe files functionality") - print("-" * 60) - - # Create a temporary file to test - with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f: - f.write("test content for validation") - temp_file = f.name - - try: - # Test reading existing file - content, tokens = read_file_content(temp_file) - - has_begin = f"--- BEGIN FILE: {temp_file} ---" in content - has_content = "test content for validation" in content - has_end = "--- END FILE:" in content - has_tokens = tokens > 0 - - print(f" ✅ BEGIN FILE found: {has_begin}") - print(f" ✅ Correct content: {has_content}") - print(f" ✅ END FILE found: {has_end}") - print(f" ✅ Tokens > 0: {has_tokens}") - - success1 = all([has_begin, has_content, has_end, has_tokens]) - - # Test nonexistent Unix path (should return FILE NOT FOUND, not path error) - content, tokens = read_file_content("/etc/nonexistent") - not_found = "--- FILE NOT FOUND:" in content - no_path_error = "Relative paths are not supported" not in content - has_tokens2 = tokens > 0 - - print(f" ✅ Nonexistent Unix file: {not_found}") - print(f" ✅ No path error: {no_path_error}") - print(f" ✅ Tokens > 0: {has_tokens2}") - - success2 = all([not_found, no_path_error, has_tokens2]) - - success = success1 and success2 - print(f"\nResult: Safe files tests {'passed' if success else 'failed'}") - - finally: - # Clean up - try: - Path(temp_file).unlink() - except Exception: - pass - - return success - - -def test_cross_platform_file_discovery(): - """Test 4: Cross-platform file discovery.""" - print("\n🧪 Test 4: Cross-platform file discovery") - print("-" * 60) - - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_path = Path(tmp_dir) - - # Create test structure - project = tmp_path / "test-project" - project.mkdir() - - (project / "README.md").write_text("# Test Project") - (project / "main.py").write_text("print('Hello')") - - src = project / "src" - src.mkdir() - (src / "app.py").write_text("# App code") - - # Test with mock MCP - def mock_is_mcp(path): - return False # No MCP in this test - - with patch("utils.file_utils.is_mcp_directory", side_effect=mock_is_mcp): - files = expand_paths([str(project)]) - - file_paths = [str(f) for f in files] - - # Use Path.parts for cross-platform checks - readme_found = any(Path(p).parts[-2:] == ("test-project", "README.md") for p in file_paths) - main_found = any(Path(p).parts[-2:] == ("test-project", "main.py") for p in file_paths) - app_found = any(Path(p).parts[-2:] == ("src", "app.py") for p in file_paths) - - print(f" ✅ README.md found: {readme_found}") - print(f" ✅ main.py found: {main_found}") - print(f" ✅ app.py found: {app_found}") - print(f" ℹ️ Files found: {len(file_paths)}") - - success = all([readme_found, main_found, app_found]) - print(f"\nResult: Cross-platform discovery {'passed' if success else 'failed'}") - - return success - - -def test_communication_simulator_fixes(): - """Test 5: Communication simulator fixes""" - print("\n🧪 Test 5: Communication simulator fixes") - print("-" * 60) - - try: - # Import and test CommunicationSimulator - from communication_simulator_test import CommunicationSimulator - - # Test that we can create an instance without logger errors - simulator = CommunicationSimulator(verbose=False, keep_logs=True) - - # Check that logger is properly initialized - has_logger = hasattr(simulator, "logger") and simulator.logger is not None - print(f" ✅ Logger initialized: {has_logger}") - - # Check that python_path is set - has_python_path = hasattr(simulator, "python_path") and simulator.python_path is not None - print(f" ✅ Python path set: {has_python_path}") - - # Check that the path detection logic includes Windows - import os - import platform - - if platform.system() == "Windows": - # Test Windows path detection - current_dir = os.getcwd() - expected_paths = [ - os.path.join(current_dir, ".zen_venv", "Scripts", "python.exe"), - os.path.join(current_dir, "venv", "Scripts", "python.exe"), - ] - - # Check if the method would detect Windows paths - windows_detection = any("Scripts" in path for path in expected_paths) - print(f" ✅ Windows path detection: {windows_detection}") - else: - windows_detection = True # Pass on non-Windows systems - print(" ✅ Windows path detection: N/A (not Windows)") - - success = all([has_logger, has_python_path, windows_detection]) - print(f"\nResult: Communication simulator {'passed' if success else 'failed'}") - - return success - - except Exception as e: - print(f" ❌ Error testing CommunicationSimulator: {e}") - print("\nResult: Communication simulator failed") - return False - - -def test_base_simulator_test_fixes(): - """Test 6: BaseSimulatorTest fixes.""" - print("\n🧪 Test 6: BaseSimulatorTest fixes") - print("-" * 60) - - try: - # Import and test BaseSimulatorTest - from simulator_tests.base_test import BaseSimulatorTest - - # Test that we can create an instance without logger errors - base_test = BaseSimulatorTest(verbose=False) - - # Check that logger is properly initialized - has_logger = hasattr(base_test, "logger") and base_test.logger is not None - print(f" ✅ Logger initialized: {has_logger}") - - # Check that python_path is set - has_python_path = hasattr(base_test, "python_path") and base_test.python_path is not None - print(f" ✅ Python path set: {has_python_path}") - - # Check that the path detection logic includes Windows - import os - import platform - - if platform.system() == "Windows": - # Test Windows path detection - current_dir = os.getcwd() - expected_path = os.path.join(current_dir, ".zen_venv", "Scripts", "python.exe") - - # Check if the method would detect Windows paths - windows_detection = "Scripts" in expected_path - print(f" ✅ Windows path detection: {windows_detection}") - else: - windows_detection = True # Pass on non-Windows systems - print(" ✅ Windows path detection: N/A (not Windows)") - - # Test that we can call methods that previously failed - try: - # Test accessing properties without calling abstract methods - # Just check that logger-related functionality works - logger_accessible = hasattr(base_test, "logger") and callable(getattr(base_test, "logger", None)) - method_callable = True - print(f" ✅ Methods callable: {method_callable}") - print(f" ✅ Logger accessible: {logger_accessible}") - except AttributeError as e: - if "logger" in str(e): - method_callable = False - print(f" ❌ Logger error still present: {e}") - else: - method_callable = True # Different error, not logger-related - print(f" ✅ No logger errors (different error): {str(e)[:50]}...") - - success = all([has_logger, has_python_path, windows_detection, method_callable]) - print(f"\nResult: BaseSimulatorTest {'passed' if success else 'failed'}") - - return success - - except Exception as e: - print(f" ❌ Error testing BaseSimulatorTest: {e}") - print("\nResult: BaseSimulatorTest failed") - return False - - -def test_shell_scripts_windows_support(): - """Test 7: Shell scripts Windows support.""" - print("\n🧪 Test 7: Shell scripts Windows support") - print("-" * 60) - - try: - # Check run_integration_tests.sh - try: - with open("run_integration_tests.sh", encoding="utf-8") as f: - run_script_content = f.read() - - has_windows_venv = 'elif [[ -f ".zen_venv/Scripts/activate" ]]; then' in run_script_content - has_windows_msg = "Using virtual environment (Windows)" in run_script_content - - print(f" ✅ run_integration_tests.sh Windows venv: {has_windows_venv}") - print(f" ✅ run_integration_tests.sh Windows message: {has_windows_msg}") - - run_script_ok = has_windows_venv and has_windows_msg - - except FileNotFoundError: - print(" ⚠️ run_integration_tests.sh not found") - run_script_ok = True # Skip if file doesn't exist - - # Check code_quality_checks.sh - try: - with open("code_quality_checks.sh", encoding="utf-8") as f: - quality_script_content = f.read() - - has_windows_python = 'elif [[ -f ".zen_venv/Scripts/python.exe" ]]; then' in quality_script_content - has_windows_tools = 'elif [[ -f ".zen_venv/Scripts/ruff.exe" ]]; then' in quality_script_content - has_windows_msg = "Using venv (Windows)" in quality_script_content - - print(f" ✅ code_quality_checks.sh Windows Python: {has_windows_python}") - print(f" ✅ code_quality_checks.sh Windows tools: {has_windows_tools}") - print(f" ✅ code_quality_checks.sh Windows message: {has_windows_msg}") - - quality_script_ok = has_windows_python and has_windows_tools and has_windows_msg - - except FileNotFoundError: - print(" ⚠️ code_quality_checks.sh not found") - quality_script_ok = True # Skip if file doesn't exist - - success = run_script_ok and quality_script_ok - print(f"\nResult: Shell scripts {'passed' if success else 'failed'}") - - return success - - except Exception as e: - print(f" ❌ Error testing shell scripts: {e}") - print("\nResult: Shell scripts failed") - return False - - -def test_docker_path_validation(): - """Test 8: Docker path validation in file_utils.py.""" - print("\n🧪 Test 8: Docker path validation") - print("-" * 60) - - try: - # Test that path_detector import is available - try: - from utils.path_detector import PathModeDetector - - detector_import = True - print(" ✅ Path detector import: True") - except ImportError as e: - detector_import = False - print(f" ❌ Path detector import: False ({e})") - - # Test Docker path validation with mocked Docker mode - try: - # Mock Docker mode - with patch.dict(os.environ, {"MCP_FILE_PATH_MODE": "docker"}): - # Reset singleton to pick up new environment - PathModeDetector._instance = None - - # Test Docker path validation - docker_path = "/app/project/test.py" - try: - from utils.file_utils import resolve_and_validate_path - - resolve_and_validate_path(docker_path) - docker_validation = True - print(" ✅ Docker path validation: True") - except ValueError as e: - if "Relative paths are not supported" in str(e): - docker_validation = False - print(" ❌ Docker path validation: False (still rejected)") - else: - docker_validation = True # Different error, not path format - print(" ✅ Docker path validation: True (security error)") - except Exception as e: - docker_validation = False - print(f" ❌ Docker path validation: Error ({e})") - - # Reset singleton after test - PathModeDetector._instance = None - - except Exception as e: - docker_validation = False - print(f" ❌ Docker path test error: {e}") - - # Test that file_utils.py contains Docker-related code - try: - with open("utils/file_utils.py", encoding="utf-8") as f: - file_utils_content = f.read() - - has_detector_import = "from utils.path_detector import get_path_detector" in file_utils_content - has_docker_check = "is_docker_path = converted_path_str.startswith" in file_utils_content - has_docker_mode = "is_docker_mode" in file_utils_content - - print(f" ✅ Has detector import: {has_detector_import}") - print(f" ✅ Has Docker path check: {has_docker_check}") - print(f" ✅ Has Docker mode check: {has_docker_mode}") - - file_content_ok = has_detector_import and has_docker_check and has_docker_mode - - except FileNotFoundError: - file_content_ok = False - print(" ❌ utils/file_utils.py not found") - - success = detector_import and docker_validation and file_content_ok - print(f"\nResult: Docker path validation {'passed' if success else 'failed'}") - - return success - - except Exception as e: - print(f" ❌ Error testing Docker path validation: {e}") - print("\nResult: Docker path validation failed") - return False - - -def test_conversation_docker_compatibility(): - """Test 9: Conversation tests Docker mode compatibility.""" - print("\n🧪 Test 9: Conversation tests Docker mode compatibility") - print("-" * 60) - - try: - # Test that test_conversation_file_features.py contains Docker fixes - try: - with open("tests/test_conversation_file_features.py", encoding="utf-8") as f: - test_content = f.read() - - has_local_mode = '"MCP_FILE_PATH_MODE": "local"' in test_content - has_detector_reset = "PathModeDetector._instance = None" in test_content - has_cache_reset = "detector._cached_mode = None" in test_content - has_subdir = "zen-mcp-server" in test_content - - print(f" ✅ Forces local mode: {has_local_mode}") - print(f" ✅ Resets PathModeDetector: {has_detector_reset}") - print(f" ✅ Resets detector cache: {has_cache_reset}") - print(f" ✅ Uses project subdirectory: {has_subdir}") - - test_content_ok = has_local_mode and has_detector_reset and has_cache_reset - - except FileNotFoundError: - test_content_ok = False - print(" ❌ tests/test_conversation_file_features.py not found") - - # Test PathModeDetector cache reset functionality - try: - from utils.path_detector import PathModeDetector, get_path_detector - - # Test that we can reset the singleton - detector1 = get_path_detector() - detector1.get_path_mode() - - # Reset and test again - PathModeDetector._instance = None - detector2 = get_path_detector() - - # Test cache reset - detector2._cached_mode = None - detector2.get_path_mode() - - reset_works = True - print(" ✅ PathModeDetector reset: True") - - except Exception as e: - reset_works = False - print(f" ❌ PathModeDetector reset: False ({e})") - - # Test environment patching works - try: - from utils.path_detector import get_path_detector - - with patch.dict(os.environ, {"MCP_FILE_PATH_MODE": "local"}): - PathModeDetector._instance = None - detector = get_path_detector() - detector._cached_mode = None - mode = detector.get_path_mode() - - env_patch_works = mode == "local" - print(f" ✅ Environment patching: {env_patch_works}") - - except Exception as e: - env_patch_works = False - print(f" ❌ Environment patching: False ({e})") - - success = test_content_ok and reset_works and env_patch_works - print(f"\nResult: Conversation Docker compatibility {'passed' if success else 'failed'}") - - return success - - except Exception as e: - print(f" ❌ Error testing conversation Docker compatibility: {e}") - print("\nResult: Conversation Docker compatibility failed") - return False - - -def main(): - """Main validation function.""" - print("🔧 Final validation of cross-platform fixes") - print("=" * 70) - print("This script validates that all fixes work on Windows.") - print("=" * 70) - - # Run all tests - results = [] - - results.append(("Home directory patterns", test_home_directory_patterns())) - results.append(("Unix path validation", test_unix_path_validation())) - results.append(("Safe files", test_safe_files_functionality())) - results.append(("Cross-platform discovery", test_cross_platform_file_discovery())) - results.append(("Communication simulator", test_communication_simulator_fixes())) - results.append(("BaseSimulatorTest", test_base_simulator_test_fixes())) - results.append(("Shell scripts Windows support", test_shell_scripts_windows_support())) - results.append(("Docker path validation", test_docker_path_validation())) - results.append(("Conversation Docker compatibility", test_conversation_docker_compatibility())) - - # Final summary - print("\n" + "=" * 70) - print("📊 FINAL SUMMARY") - print("=" * 70) - - passed_tests = 0 - for test_name, success in results: - status = "PASSED" if success else "FAILED" - print(f"{status:<10} {test_name}") - if success: - passed_tests += 1 - - total_tests = len(results) - print(f"\nOverall result: {passed_tests}/{total_tests} test groups passed") - - if passed_tests == total_tests: - print("\n🎉 COMPLETE SUCCESS!") - print("All cross-platform fixes work correctly.") - return 0 - else: - print("\n❌ FAILURES DETECTED") - print("Some fixes need adjustments.") - return 1 - - -if __name__ == "__main__": - sys.exit(main())