diff --git a/.gitignore b/.gitignore index 8c06000c..e90fde6a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Desktop.ini .env .env.* !.env.example +/config.json *.pem *.key *.crt diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index b9969020..0e2a7b42 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -15,6 +15,7 @@ if str(_PARENT_DIR) not in sys.path: sys.path.insert(0, str(_PARENT_DIR)) from core.auth import get_auth_token, get_auth_token_source +from core.dependency_validator import validate_platform_dependencies def import_dotenv(): @@ -154,6 +155,9 @@ def validate_environment(spec_dir: Path) -> bool: Returns: True if valid, False otherwise (with error messages printed) """ + # Validate platform-specific dependencies first (exits if missing) + validate_platform_dependencies() + valid = True # Check for OAuth token (API keys are not supported) diff --git a/apps/backend/core/dependency_validator.py b/apps/backend/core/dependency_validator.py new file mode 100644 index 00000000..8517cb36 --- /dev/null +++ b/apps/backend/core/dependency_validator.py @@ -0,0 +1,50 @@ +""" +Dependency Validator +==================== + +Validates platform-specific dependencies are installed before running agents. +""" + +import sys +from pathlib import Path + + +def validate_platform_dependencies() -> None: + """ + Validate that platform-specific dependencies are installed. + + Raises: + SystemExit: If required platform-specific dependencies are missing, + with helpful installation instructions. + """ + # Check Windows-specific dependencies + if sys.platform == "win32" and sys.version_info >= (3, 12): + try: + import pywintypes # noqa: F401 + except ImportError: + _exit_with_pywin32_error() + + +def _exit_with_pywin32_error() -> None: + """Exit with helpful error message for missing pywin32.""" + # Use sys.prefix to detect the virtual environment path + # This works for venv and poetry environments + venv_activate = Path(sys.prefix) / "Scripts" / "activate" + + sys.exit( + "Error: Required Windows dependency 'pywin32' is not installed.\n" + "\n" + "Auto Claude requires pywin32 on Windows for LadybugDB/Graphiti memory integration.\n" + "\n" + "To fix this:\n" + "1. Activate your virtual environment:\n" + f" {venv_activate}\n" + "\n" + "2. Install pywin32:\n" + " pip install pywin32>=306\n" + "\n" + " Or reinstall all dependencies:\n" + " pip install -r requirements.txt\n" + "\n" + f"Current Python: {sys.executable}\n" + ) diff --git a/apps/backend/integrations/graphiti/queries_pkg/client.py b/apps/backend/integrations/graphiti/queries_pkg/client.py index c1961484..3808d9d5 100644 --- a/apps/backend/integrations/graphiti/queries_pkg/client.py +++ b/apps/backend/integrations/graphiti/queries_pkg/client.py @@ -34,8 +34,25 @@ def _apply_ladybug_monkeypatch() -> bool: sys.modules["kuzu"] = real_ladybug logger.info("Applied LadybugDB monkeypatch (kuzu -> real_ladybug)") return True - except ImportError: - pass + except ImportError as e: + logger.debug(f"LadybugDB import failed: {e}") + # On Windows with Python 3.12+, provide more specific error details + # (pywin32 is only required for Python 3.12+ per requirements.txt) + if sys.platform == "win32" and sys.version_info >= (3, 12): + # Check if it's the pywin32 error using both name attribute and string match + # for robustness across Python versions + is_pywin32_error = ( + (hasattr(e, "name") and e.name in ("pywintypes", "pywin32", "win32api")) + or "pywintypes" in str(e) + or "pywin32" in str(e) + ) + if is_pywin32_error: + logger.error( + "LadybugDB requires pywin32 on Windows. " + "Install with: pip install pywin32>=306" + ) + else: + logger.debug(f"Windows-specific import issue: {e}") # Fall back to native kuzu try: diff --git a/apps/backend/requirements.txt b/apps/backend/requirements.txt index 59aec7b0..95c8a1ea 100644 --- a/apps/backend/requirements.txt +++ b/apps/backend/requirements.txt @@ -10,6 +10,10 @@ tomli>=2.0.0; python_version < "3.11" real_ladybug>=0.13.0; python_version >= "3.12" graphiti-core>=0.5.0; python_version >= "3.12" +# Windows-specific dependency for LadybugDB/Graphiti +# pywin32 provides Windows system bindings required by real_ladybug +pywin32>=306; sys_platform == "win32" and python_version >= "3.12" + # Google AI (optional - for Gemini LLM and embeddings) google-generativeai>=0.8.0 diff --git a/tests/test_github_pr_review.py b/tests/test_github_pr_review.py index 25b72fe4..741d69bc 100644 --- a/tests/test_github_pr_review.py +++ b/tests/test_github_pr_review.py @@ -101,13 +101,11 @@ def mock_bot_detector(tmp_path): class TestPRReviewResult: """Test PRReviewResult model.""" - def test_save_and_load(self, temp_github_dir, sample_review_result): + @pytest.mark.asyncio + async def test_save_and_load(self, temp_github_dir, sample_review_result): """Test saving and loading review result.""" # Save - import asyncio - asyncio.get_event_loop().run_until_complete( - sample_review_result.save(temp_github_dir) - ) + await sample_review_result.save(temp_github_dir) # Verify file exists review_file = temp_github_dir / "pr" / f"review_{sample_review_result.pr_number}.json" @@ -337,14 +335,11 @@ class TestBotDetectionIntegration: class TestOrchestratorSkipLogic: """Test orchestrator behavior when bot detection skips.""" - def test_skip_returns_existing_review(self, temp_github_dir, sample_review_result): + @pytest.mark.asyncio + async def test_skip_returns_existing_review(self, temp_github_dir, sample_review_result): """Test that skipping 'Already reviewed' returns existing review.""" - import asyncio - # Save existing review - asyncio.get_event_loop().run_until_complete( - sample_review_result.save(temp_github_dir) - ) + await sample_review_result.save(temp_github_dir) # Simulate the orchestrator logic for "Already reviewed" skip skip_reason = "Already reviewed commit abc123" @@ -466,19 +461,16 @@ class TestPostedFindingsTracking: assert sample_review_result.has_posted_findings is True assert len(sample_review_result.posted_finding_ids) == 1 - def test_posted_findings_serialization(self, temp_github_dir, sample_review_result): + @pytest.mark.asyncio + async def test_posted_findings_serialization(self, temp_github_dir, sample_review_result): """Test that posted findings are serialized correctly.""" - import asyncio - # Set posted findings sample_review_result.has_posted_findings = True sample_review_result.posted_finding_ids = ["finding-001"] sample_review_result.posted_at = "2025-01-01T10:00:00" # Save - asyncio.get_event_loop().run_until_complete( - sample_review_result.save(temp_github_dir) - ) + await sample_review_result.save(temp_github_dir) # Load and verify loaded = PRReviewResult.load(temp_github_dir, sample_review_result.pr_number)