diff --git a/INSTALL.md b/INSTALL.md index 06b1c64..876c7a2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -15,9 +15,10 @@ python --version -**🪟 Windows 10/11** +**🪟 Windows 10/11** or **🍎 macOS** ```bash -node --version +# Check your OS +uname -s ``` @@ -43,10 +44,10 @@ git --version -### Quick Setup +### Quick Setup (Windows) -1. Clone the repository : -```bash +1. Clone the repository : +```bash git clone https://github.com/MLSAKIIT/pixly.git cd pixly ``` @@ -54,6 +55,37 @@ cd pixly ```bash .\setup.bat ``` + +### macOS Setup + +#### Prerequisites: CustomTkinter Support + +Pixly uses CustomTkinter for the GUI overlay, which requires the underlying Tkinter library to be available in your Python installation. If you're using `pyenv`, you need to install Python with Tkinter support: + +1. **Install tcl-tk via Homebrew**: +```bash +brew install tcl-tk +``` + +2. **Reinstall Python 3.11+ with Tkinter support**: +```bash +export PATH="/opt/homebrew/opt/tcl-tk/bin:$PATH" +export LDFLAGS="-L/opt/homebrew/opt/tcl-tk/lib" +export CPPFLAGS="-I/opt/homebrew/opt/tcl-tk/include" +export PKG_CONFIG_PATH="/opt/homebrew/opt/tcl-tk/lib/pkgconfig" +export PYTHON_CONFIGURE_OPTS="--with-tcltk-includes='-I/opt/homebrew/opt/tcl-tk/include' --with-tcltk-libs='-L/opt/homebrew/opt/tcl-tk/lib -ltcl9.0 -ltk9.0'" + +pyenv install 3.11.7 --force +``` + +3. **Verify the underlying Tkinter library is working**: +```bash +python3 -m tkinter +``` +If a small window appears, Tkinter is working correctly and CustomTkinter will be able to use it! + +4. **Continue with Manual Setup below** + ### Manual Setup 1. Clone the repository : ```bash @@ -80,26 +112,60 @@ GEMINI_API_KEY=your_gemini_key_here 1. Make a folder called `vector_db` -2. Start the application, Create two powershell terminals +2. Start the application + +**On Windows** - Create two PowerShell terminals: Terminal 1 - Start Backend: ```bash uv run run.py ``` Wait for the backend to start then in Terminal 2 - Start Frontend: -```bash +```bash uv run overlay.py ``` -## Debugging +**On macOS** - Create two terminal windows: + +Terminal 1 - Start Backend: +```bash +uv run run.py +``` +Wait for the backend to start then in Terminal 2 - Start Frontend: +```bash +uv run overlay.py +``` + +## Troubleshooting + +### macOS: "ModuleNotFoundError: No module named '_tkinter'" + +This error occurs when Python doesn't have the underlying Tkinter library, which is required for CustomTkinter to function. Follow the **macOS Setup** section above to reinstall Python with Tkinter support. + +Quick fix: +```bash +brew install python-tk@3.11 +# Then reinstall dependencies +uv sync +``` + +### macOS: Platform-specific dependencies + +The project automatically installs platform-specific dependencies: +- **Windows**: `pywin32` for window detection +- **macOS**: `pyobjc-framework-Quartz` for window detection + +No manual intervention needed - `uv sync` handles this automatically! + +## Debugging -To test the various parts of the backend pipeline : +To test the various parts of the backend pipeline: -1. Start the server in Terminal 1 : +1. Start the server in Terminal 1: ```bash uv run run.py ``` -1. Start the test script in Terminal 2 : +2. Start the test script in Terminal 2: ```bash uv run test_system.py ``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1cdf261..62d0efd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "pillow>=10.0.0", "dotenv>=0.9.9", "psutil>=5.9.0", - "pywin32>=306", + "pywin32>=306; sys_platform == 'win32'", + "pyobjc-framework-Quartz>=10.0; sys_platform == 'darwin'", "cryptography>=41.0.0", "chromadb>=0.4.0", "sentence-transformers>=2.2.2", diff --git a/services/screenshot.py b/services/screenshot.py index a8d309a..b369c01 100644 --- a/services/screenshot.py +++ b/services/screenshot.py @@ -1,15 +1,29 @@ import os +import sys import sqlite3 import hashlib from datetime import datetime import threading import time import psutil -import win32gui -import win32process from PIL import ImageGrab from cryptography.fernet import Fernet +# Platform-specific imports +if sys.platform == 'win32': + import win32gui + import win32process +elif sys.platform == 'darwin': + try: + from Quartz import ( + CGWindowListCopyWindowInfo, + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID + ) + from AppKit import NSWorkspace + except ImportError: + print("Warning: pyobjc-framework-Quartz not installed. Window detection will be limited on macOS.") + class ScreenshotCapture: def __init__(self, db_path="screenshots.db", interval=30): """ @@ -74,27 +88,68 @@ def _init_database(self): def _get_active_window_info(self): """Get information about the currently active window.""" try: - # Get the active window - hwnd = win32gui.GetForegroundWindow() - window_title = win32gui.GetWindowText(hwnd) - - # Get process information - _, pid = win32process.GetWindowThreadProcessId(hwnd) - process = psutil.Process(pid) - application = process.name() - - return { - 'application': application, - 'window_title': window_title, - 'pid': pid - } + if sys.platform == 'win32': + return self._get_active_window_info_windows() + elif sys.platform == 'darwin': + return self._get_active_window_info_macos() + else: + return self._get_default_window_info() except Exception as e: print(f"Error getting window info: {e}") - return { - 'application': 'Unknown', - 'window_title': 'Unknown', - 'pid': 0 - } + return self._get_default_window_info() + + def _get_active_window_info_windows(self): + """Get active window info on Windows.""" + hwnd = win32gui.GetForegroundWindow() + window_title = win32gui.GetWindowText(hwnd) + + # Get process information + _, pid = win32process.GetWindowThreadProcessId(hwnd) + process = psutil.Process(pid) + application = process.name() + + return { + 'application': application, + 'window_title': window_title, + 'pid': pid + } + + def _get_active_window_info_macos(self): + """Get active window info on macOS.""" + workspace = NSWorkspace.sharedWorkspace() + active_app = workspace.activeApplication() + + application = active_app['NSApplicationName'] + pid = active_app['NSApplicationProcessIdentifier'] + + # Get window title from window list + window_title = 'Unknown' + try: + window_list = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID + ) + for window in window_list: + if window.get('kCGWindowOwnerPID') == pid: + window_title = window.get('kCGWindowName', 'Unknown') + if window_title and window_title != 'Unknown': + break + except Exception as e: + print(f"Error getting window title: {e}") + + return { + 'application': application, + 'window_title': window_title, + 'pid': pid + } + + def _get_default_window_info(self): + """Return default window info when platform detection fails.""" + return { + 'application': 'Unknown', + 'window_title': 'Unknown', + 'pid': 0 + } def _capture_screenshot(self): """Capture a screenshot and return the image data.""" diff --git a/tests/unit/test_backend.py b/tests/unit/test_backend.py index ca736b8..770e519 100644 --- a/tests/unit/test_backend.py +++ b/tests/unit/test_backend.py @@ -18,7 +18,7 @@ try: from backend.backend import app - from backend.chatbot import chat_with_gemini, set_api_key + from services.chatbot import chat_with_gemini, set_api_key from services.screenshot import get_recent_screenshots, get_screenshot_by_id, get_screenshot_stats, delete_screenshot from services.game_detection import detect_current_game, get_available_games as get_detection_games from services.knowledge_manager import get_available_games as get_csv_games, validate_csv_structure @@ -41,7 +41,7 @@ def test_chat_endpoint_success(self, client, sample_chat_message): """Test successful chat endpoint.""" mock_response = {"response": "Test response from Gemini"} - with patch('backend.backend.chat_with_gemini', return_value=mock_response) as mock_chat: + with patch('routers.chat.chat_with_gemini', return_value=mock_response) as mock_chat: response = client.post("/chat", json=sample_chat_message) assert response.status_code == 200 @@ -54,7 +54,7 @@ def test_chat_endpoint_with_image(self, client, sample_chat_message_with_image): """Test chat endpoint with image data.""" mock_response = {"response": "I can see the image"} - with patch('backend.backend.chat_with_gemini', return_value=mock_response) as mock_chat: + with patch('routers.chat.chat_with_gemini', return_value=mock_response) as mock_chat: response = client.post("/chat", json=sample_chat_message_with_image) assert response.status_code == 200 @@ -102,7 +102,7 @@ def test_task_b_endpoint(self, client): @pytest.mark.api def test_screenshots_recent_endpoint(self, client, mock_screenshot_records): """Test recent screenshots endpoint.""" - with patch('backend.backend.get_recent_screenshots', return_value=mock_screenshot_records) as mock_get: + with patch('routers.screenshot.get_recent_screenshots', return_value=mock_screenshot_records) as mock_get: response = client.get("/screenshots/recent") assert response.status_code == 200 @@ -117,7 +117,7 @@ def test_screenshots_recent_endpoint(self, client, mock_screenshot_records): @pytest.mark.api def test_screenshots_recent_with_limit(self, client, mock_screenshot_records): """Test recent screenshots endpoint with custom limit.""" - with patch('backend.backend.get_recent_screenshots', return_value=mock_screenshot_records[:2]) as mock_get: + with patch('routers.screenshot.get_recent_screenshots', return_value=mock_screenshot_records[:2]) as mock_get: response = client.get("/screenshots/recent?limit=2") assert response.status_code == 200 @@ -132,7 +132,7 @@ def test_screenshots_recent_with_application_filter(self, client, mock_screensho """Test recent screenshots endpoint with application filter.""" filtered_records = [record for record in mock_screenshot_records if record[2] == 'minecraft.exe'] - with patch('backend.backend.get_recent_screenshots', return_value=filtered_records) as mock_get: + with patch('routers.screenshot.get_recent_screenshots', return_value=filtered_records) as mock_get: response = client.get("/screenshots/recent?application=minecraft.exe") assert response.status_code == 200 @@ -144,7 +144,7 @@ def test_screenshots_recent_with_application_filter(self, client, mock_screensho @pytest.mark.api def test_screenshot_by_id_endpoint(self, client, mock_screenshot_data): """Test get screenshot by ID endpoint.""" - with patch('backend.backend.get_screenshot_by_id', return_value=mock_screenshot_data['image_data']) as mock_get: + with patch('routers.screenshot.get_screenshot_by_id', return_value=mock_screenshot_data['image_data']) as mock_get: response = client.get("/screenshots/123") assert response.status_code == 200 @@ -157,7 +157,7 @@ def test_screenshot_by_id_endpoint(self, client, mock_screenshot_data): @pytest.mark.api def test_screenshot_by_id_not_found(self, client): """Test get screenshot by ID endpoint when screenshot not found.""" - with patch('backend.backend.get_screenshot_by_id', return_value=None) as mock_get: + with patch('routers.screenshot.get_screenshot_by_id', return_value=None) as mock_get: response = client.get("/screenshots/999") assert response.status_code == 200 @@ -176,7 +176,7 @@ def test_screenshot_stats_endpoint(self, client): 'date_range': ['2024-01-01T10:00:00', '2024-01-01T12:00:00'] } - with patch('backend.backend.get_screenshot_stats', return_value=mock_stats) as mock_get: + with patch('routers.screenshot.get_screenshot_stats', return_value=mock_stats) as mock_get: response = client.get("/screenshots/stats") assert response.status_code == 200 @@ -189,7 +189,7 @@ def test_screenshot_stats_endpoint(self, client): @pytest.mark.api def test_delete_screenshot_endpoint(self, client): """Test delete screenshot endpoint.""" - with patch('backend.backend.delete_screenshot', return_value=True) as mock_delete: + with patch('routers.screenshot.delete_screenshot', return_value=True) as mock_delete: response = client.delete("/screenshots/123") assert response.status_code == 200 @@ -202,7 +202,7 @@ def test_delete_screenshot_endpoint(self, client): @pytest.mark.api def test_delete_screenshot_not_found(self, client): """Test delete screenshot endpoint when screenshot not found.""" - with patch('backend.backend.delete_screenshot', return_value=False) as mock_delete: + with patch('routers.screenshot.delete_screenshot', return_value=False) as mock_delete: response = client.delete("/screenshots/999") assert response.status_code == 404 @@ -213,7 +213,7 @@ def test_delete_screenshot_not_found(self, client): @pytest.mark.api def test_game_detection_endpoint(self, client): """Test game detection endpoint.""" - with patch('backend.backend.detect_current_game', return_value="minecraft") as mock_detect: + with patch('routers.game_detection.detect_current_game', return_value="minecraft") as mock_detect: response = client.post("/games/detect", json={"message": "How do I craft in minecraft?"}) assert response.status_code == 200 @@ -226,7 +226,7 @@ def test_game_detection_endpoint(self, client): @pytest.mark.api def test_game_detection_endpoint_no_message(self, client): """Test game detection endpoint without message.""" - with patch('backend.backend.detect_current_game', return_value="minecraft") as mock_detect: + with patch('routers.game_detection.detect_current_game', return_value="minecraft") as mock_detect: response = client.post("/games/detect", json={}) assert response.status_code == 200 @@ -239,7 +239,7 @@ def test_game_detection_endpoint_no_message(self, client): @pytest.mark.api def test_game_detection_endpoint_no_game_detected(self, client): """Test game detection endpoint when no game is detected.""" - with patch('backend.backend.detect_current_game', return_value=None) as mock_detect: + with patch('routers.game_detection.detect_current_game', return_value=None) as mock_detect: response = client.post("/games/detect", json={"message": "random text"}) assert response.status_code == 200 @@ -256,9 +256,9 @@ def test_games_available_endpoint(self, client): mock_csv_games = ["minecraft", "black_myth_wukong"] mock_vector_games = ["minecraft", "elden_ring"] - with patch('backend.backend.get_detection_games', return_value=mock_detection_games) as mock_detect, \ - patch('backend.backend.get_csv_games', return_value=mock_csv_games) as mock_csv, \ - patch('backend.backend.list_available_games', return_value=mock_vector_games) as mock_vector: + with patch('routers.game_detection.get_detection_games', return_value=mock_detection_games) as mock_detect, \ + patch('routers.game_detection.get_csv_games', return_value=mock_csv_games) as mock_csv, \ + patch('routers.game_detection.list_available_games', return_value=mock_vector_games) as mock_vector: response = client.get("/games/list") assert response.status_code == 200 @@ -305,9 +305,9 @@ def test_knowledge_add_endpoint(self, client): """Test add game knowledge endpoint.""" mock_stats = {"wiki": 10, "youtube": 5, "forum": 15} - with patch('backend.backend.validate_csv_structure', return_value=(True, [])) as mock_validate, \ - patch('backend.backend.add_game_knowledge', return_value=True) as mock_add, \ - patch('backend.backend.get_game_stats', return_value=mock_stats) as mock_stats_func: + with patch('routers.game_detection.validate_csv_structure', return_value=(True, [])) as mock_validate, \ + patch('routers.game_detection.add_game_knowledge', return_value=True) as mock_add, \ + patch('routers.game_detection.get_game_stats', return_value=mock_stats) as mock_stats_func: response = client.post("/games/minecraft/knowledge/process") assert response.status_code == 200 @@ -322,8 +322,8 @@ def test_knowledge_add_endpoint(self, client): @pytest.mark.api def test_knowledge_add_endpoint_failure(self, client): """Test add game knowledge endpoint with failure.""" - with patch('backend.backend.validate_csv_structure', return_value=(True, [])) as mock_validate, \ - patch('backend.backend.add_game_knowledge', return_value=False) as mock_add: + with patch('routers.game_detection.validate_csv_structure', return_value=(True, [])) as mock_validate, \ + patch('routers.game_detection.add_game_knowledge', return_value=False) as mock_add: response = client.post("/games/nonexistent_game/knowledge/process") assert response.status_code == 500 @@ -343,7 +343,7 @@ def test_knowledge_add_endpoint_missing_game_name(self, client): @pytest.mark.api def test_knowledge_search_endpoint(self, client, mock_vector_search_results): """Test search knowledge endpoint.""" - with patch('backend.backend.search_knowledge', return_value=mock_vector_search_results) as mock_search: + with patch('routers.game_detection.search_knowledge', return_value=mock_vector_search_results) as mock_search: response = client.post("/games/minecraft/knowledge/search", json={ "query": "How do I craft items?", "limit": 5 @@ -361,7 +361,7 @@ def test_knowledge_search_endpoint(self, client, mock_vector_search_results): @pytest.mark.api def test_knowledge_search_endpoint_with_content_types(self, client, mock_vector_search_results): """Test search knowledge endpoint with content types.""" - with patch('backend.backend.search_knowledge', return_value=mock_vector_search_results) as mock_search: + with patch('routers.game_detection.search_knowledge', return_value=mock_vector_search_results) as mock_search: response = client.post("/games/minecraft/knowledge/search", json={ "query": "How do I craft items?", "content_types": ["wiki", "forum"], @@ -388,7 +388,7 @@ def test_knowledge_stats_endpoint(self, client): """Test knowledge statistics endpoint.""" mock_stats = {"wiki": 10, "youtube": 5, "forum": 15} - with patch('backend.backend.get_game_stats', return_value=mock_stats) as mock_get: + with patch('routers.game_detection.get_game_stats', return_value=mock_stats) as mock_get: response = client.get("/games/minecraft/knowledge/stats") assert response.status_code == 200 @@ -409,7 +409,7 @@ def test_knowledge_games_endpoint(self, client): @pytest.mark.api def test_api_key_update_endpoint(self, client, sample_api_key_request): """Test API key update endpoint.""" - with patch('backend.backend.set_api_key', return_value=True) as mock_set, \ + with patch('routers.settings.set_api_key', return_value=True) as mock_set, \ patch('builtins.open', create=True) as mock_open, \ patch('os.path.exists', return_value=True): response = client.post("/settings/api-key", json=sample_api_key_request) @@ -424,7 +424,7 @@ def test_api_key_update_endpoint(self, client, sample_api_key_request): @pytest.mark.api def test_api_key_update_endpoint_failure(self, client, sample_api_key_request): """Test API key update endpoint with failure.""" - with patch('backend.backend.set_api_key', return_value=False) as mock_set, \ + with patch('routers.settings.set_api_key', return_value=False) as mock_set, \ patch('builtins.open', create=True) as mock_open, \ patch('os.path.exists', return_value=True): response = client.post("/settings/api-key", json=sample_api_key_request) @@ -446,7 +446,7 @@ def test_api_key_update_endpoint_missing_key(self, client): @pytest.mark.api def test_api_key_update_endpoint_exception(self, client, sample_api_key_request): """Test API key update endpoint with exception.""" - with patch('backend.backend.set_api_key', side_effect=Exception("API Error")) as mock_set: + with patch('routers.settings.set_api_key', side_effect=Exception("API Error")) as mock_set: response = client.post("/settings/api-key", json=sample_api_key_request) assert response.status_code == 500 @@ -469,7 +469,7 @@ def test_full_chat_workflow(self, client, sample_chat_message): """Test complete chat workflow through API.""" mock_response = {"response": "Here's how to craft a sword in Minecraft..."} - with patch('backend.backend.chat_with_gemini', return_value=mock_response) as mock_chat: + with patch('routers.chat.chat_with_gemini', return_value=mock_response) as mock_chat: # Send chat request response = client.post("/chat", json=sample_chat_message) @@ -483,27 +483,27 @@ def test_full_chat_workflow(self, client, sample_chat_message): def test_full_screenshot_workflow(self, client, mock_screenshot_records, mock_screenshot_data): """Test complete screenshot workflow through API.""" # Get recent screenshots - with patch('backend.backend.get_recent_screenshots', return_value=mock_screenshot_records): + with patch('routers.screenshot.get_recent_screenshots', return_value=mock_screenshot_records): response = client.get("/screenshots/recent") assert response.status_code == 200 data = response.json() assert len(data["screenshots"]) == 3 # Get screenshot by ID - with patch('backend.backend.get_screenshot_by_id', return_value=mock_screenshot_data['image_data']): + with patch('routers.screenshot.get_screenshot_by_id', return_value=mock_screenshot_data['image_data']): response = client.get("/screenshots/1") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" # Get screenshot stats mock_stats = {'total_screenshots': 3, 'applications': [], 'date_range': [None, None]} - with patch('backend.backend.get_screenshot_stats', return_value=mock_stats): + with patch('routers.screenshot.get_screenshot_stats', return_value=mock_stats): response = client.get("/screenshots/stats") assert response.status_code == 200 assert response.json() == {"status": "ok", "stats": mock_stats} # Delete screenshot - with patch('backend.backend.delete_screenshot', return_value=True): + with patch('routers.screenshot.delete_screenshot', return_value=True): response = client.delete("/screenshots/1") assert response.status_code == 200 assert response.json()["message"] == "Deleted screenshot 1" @@ -513,13 +513,13 @@ def test_full_screenshot_workflow(self, client, mock_screenshot_records, mock_sc def test_full_knowledge_workflow(self, client, mock_vector_search_results): """Test complete knowledge workflow through API.""" # Add knowledge - with patch('backend.backend.add_game_knowledge', return_value=True): + with patch('routers.game_detection.add_game_knowledge', return_value=True): response = client.post("/games/minecraft/knowledge/process") assert response.status_code == 200 assert response.json()["status"] == "ok" # Search knowledge - with patch('backend.backend.search_knowledge', return_value=mock_vector_search_results): + with patch('routers.game_detection.search_knowledge', return_value=mock_vector_search_results): response = client.post("/games/minecraft/knowledge/search", json={ "query": "How do I craft items?", "limit": 5 @@ -531,14 +531,14 @@ def test_full_knowledge_workflow(self, client, mock_vector_search_results): # Get knowledge stats mock_stats = {"wiki": 10, "youtube": 5, "forum": 15} - with patch('backend.backend.get_game_stats', return_value=mock_stats): + with patch('routers.game_detection.get_game_stats', return_value=mock_stats): response = client.get("/games/minecraft/knowledge/stats") assert response.status_code == 200 assert response.json() == {"status": "ok", "game_name": "minecraft", "stats": mock_stats} # List available games mock_games = ["minecraft", "elden_ring"] - with patch('backend.backend.list_available_games', return_value=mock_games): + with patch('routers.game_detection.list_available_games', return_value=mock_games): response = client.get("/games/list") assert response.status_code == 200 data = response.json() @@ -561,7 +561,7 @@ def test_chat_endpoint_with_very_long_message(self, client): long_message = "This is a very long message. " * 1000 mock_response = {"response": "Long message processed"} - with patch('backend.backend.chat_with_gemini', return_value=mock_response): + with patch('routers.chat.chat_with_gemini', return_value=mock_response): response = client.post("/chat", json={"message": long_message}) assert response.status_code == 200 @@ -574,7 +574,7 @@ def test_chat_endpoint_with_special_characters(self, client): special_message = "Hello! 🎮 How do I craft? @#$%^&*()" mock_response = {"response": "Special characters handled"} - with patch('backend.backend.chat_with_gemini', return_value=mock_response): + with patch('routers.chat.chat_with_gemini', return_value=mock_response): response = client.post("/chat", json={"message": special_message}) assert response.status_code == 200 @@ -592,7 +592,7 @@ def test_screenshot_endpoint_with_invalid_id(self, client): @pytest.mark.api def test_screenshot_endpoint_with_negative_id(self, client): """Test screenshot endpoint with negative ID.""" - with patch('backend.backend.get_screenshot_by_id', return_value=None): + with patch('routers.screenshot.get_screenshot_by_id', return_value=None): response = client.get("/screenshots/-1") assert response.status_code == 200 # FastAPI accepts negative integers @@ -601,7 +601,7 @@ def test_screenshot_endpoint_with_negative_id(self, client): @pytest.mark.api def test_knowledge_search_with_empty_query(self, client): """Test knowledge search with empty query.""" - with patch('backend.backend.search_knowledge', return_value=[]): + with patch('routers.game_detection.search_knowledge', return_value=[]): response = client.post("/games/minecraft/knowledge/search", json={ "query": "", "limit": 5 @@ -618,7 +618,7 @@ def test_knowledge_search_with_very_long_query(self, client): long_query = "This is a very long query. " * 100 mock_results = [] - with patch('backend.backend.search_knowledge', return_value=mock_results): + with patch('routers.game_detection.search_knowledge', return_value=mock_results): response = client.post("/games/minecraft/knowledge/search", json={ "query": long_query, "limit": 5 @@ -643,7 +643,7 @@ def test_api_key_update_with_very_long_key(self, client): long_key = "a" * 10000 mock_response = {"status": "ok", "message": "API key updated"} - with patch('backend.backend.set_api_key', return_value=True): + with patch('routers.settings.set_api_key', return_value=True): response = client.post("/settings/api-key", json={"api_key": long_key}) assert response.status_code == 200 @@ -655,7 +655,7 @@ def test_game_detection_with_unicode_message(self, client): """Test game detection with unicode message.""" unicode_message = "How do I craft in minecraft? 🎮 中文测试" - with patch('backend.backend.detect_current_game', return_value="minecraft"): + with patch('routers.game_detection.detect_current_game', return_value="minecraft"): response = client.post("/games/detect", json={"message": unicode_message}) assert response.status_code == 200 @@ -666,7 +666,7 @@ def test_game_detection_with_unicode_message(self, client): @pytest.mark.api def test_csv_validation_with_special_characters(self, client): """Test CSV validation with special characters in game name.""" - with patch('backend.backend.validate_csv_structure', return_value=(True, [])): + with patch('routers.game_detection.validate_csv_structure', return_value=(True, [])): response = client.get("/games/test-game_123/knowledge/validate") assert response.status_code == 200 diff --git a/tests/unit/test_chatbot.py b/tests/unit/test_chatbot.py index 679fb46..abc07f1 100644 --- a/tests/unit/test_chatbot.py +++ b/tests/unit/test_chatbot.py @@ -28,8 +28,8 @@ class TestSetApiKey: @pytest.mark.unit def test_set_api_key_success(self, mock_environment_variables): """Test successful API key setting.""" - with patch('backend.chatbot.genai.configure') as mock_configure, \ - patch('backend.chatbot.genai.GenerativeModel') as mock_model: + with patch('services.chatbot.genai.configure') as mock_configure, \ + patch('services.chatbot.genai.GenerativeModel') as mock_model: result = set_api_key("test_api_key_12345") @@ -54,7 +54,7 @@ def test_set_api_key_none_key(self, mock_environment_variables): @pytest.mark.unit def test_set_api_key_exception(self, mock_environment_variables): """Test API key setting with exception.""" - with patch('backend.chatbot.genai.configure', side_effect=Exception("API Error")): + with patch('services.chatbot.genai.configure', side_effect=Exception("API Error")): result = set_api_key("test_key") assert result is False @@ -70,8 +70,8 @@ async def test_chat_with_gemini_basic_message(self, mock_environment_variables): mock_response = Mock() mock_response.text = "Test response from Gemini" - with patch('backend.chatbot.detect_current_game', return_value=None) as mock_detect, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate: + with patch('services.chatbot.detect_current_game', return_value=None) as mock_detect, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate: result = await chat_with_gemini("Hello, how are you?") @@ -87,8 +87,8 @@ async def test_chat_with_gemini_with_image(self, mock_environment_variables, moc mock_response = Mock() mock_response.text = "I can see a test image" - with patch('backend.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate, \ + with patch('services.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate, \ patch('PIL.Image.open') as mock_image_open, \ patch('base64.b64decode', return_value=b'decoded_image_data') as mock_b64decode: @@ -121,10 +121,10 @@ async def test_chat_with_gemini_screenshot_keywords(self, mock_environment_varia 'applications': [('minecraft.exe', 1), ('chrome.exe', 1)] } - with patch('backend.chatbot.detect_current_game', return_value=None) as mock_detect, \ - patch('backend.chatbot.get_recent_screenshots', return_value=mock_screenshots) as mock_recent, \ - patch('backend.chatbot.get_screenshot_stats', return_value=mock_stats) as mock_stats_func, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate: + with patch('services.chatbot.detect_current_game', return_value=None) as mock_detect, \ + patch('services.chatbot.get_recent_screenshots', return_value=mock_screenshots) as mock_recent, \ + patch('services.chatbot.get_screenshot_stats', return_value=mock_stats) as mock_stats_func, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate: result = await chat_with_gemini("Show me my recent screenshots") @@ -155,9 +155,9 @@ async def test_chat_with_gemini_with_game_detection(self, mock_environment_varia } ] - with patch('backend.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ - patch('backend.chatbot.search_knowledge', return_value=mock_knowledge_results) as mock_search, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate: + with patch('services.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ + patch('services.chatbot.search_knowledge', return_value=mock_knowledge_results) as mock_search, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate: result = await chat_with_gemini("How do I craft items?") @@ -178,9 +178,9 @@ async def test_chat_with_gemini_knowledge_search_error(self, mock_environment_va mock_response = Mock() mock_response.text = "I can help with Minecraft" - with patch('backend.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ - patch('backend.chatbot.search_knowledge', side_effect=Exception("Search error")) as mock_search, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate: + with patch('services.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ + patch('services.chatbot.search_knowledge', side_effect=Exception("Search error")) as mock_search, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate: result = await chat_with_gemini("How do I craft items?") @@ -196,7 +196,7 @@ async def test_chat_with_gemini_knowledge_search_error(self, mock_environment_va @pytest.mark.asyncio async def test_chat_with_gemini_exception_handling(self, mock_environment_variables): """Test chat functionality with exception handling.""" - with patch('backend.chatbot.detect_current_game', side_effect=Exception("Detection error")): + with patch('services.chatbot.detect_current_game', side_effect=Exception("Detection error")): result = await chat_with_gemini("Hello") assert "response" in result @@ -206,7 +206,7 @@ async def test_chat_with_gemini_exception_handling(self, mock_environment_variab @pytest.mark.asyncio async def test_chat_with_gemini_image_processing_error(self, mock_environment_variables): """Test chat functionality when image processing fails.""" - with patch('backend.chatbot.detect_current_game', return_value=None), \ + with patch('services.chatbot.detect_current_game', return_value=None), \ patch('base64.b64decode', side_effect=Exception("Decode error")): result = await chat_with_gemini("What do you see?", "invalid_base64") @@ -218,8 +218,8 @@ async def test_chat_with_gemini_image_processing_error(self, mock_environment_va @pytest.mark.asyncio async def test_chat_with_gemini_gemini_api_error(self, mock_environment_variables): """Test chat functionality when Gemini API fails.""" - with patch('backend.chatbot.detect_current_game', return_value=None), \ - patch('backend.chatbot.model.generate_content', side_effect=Exception("API Error")): + with patch('services.chatbot.detect_current_game', return_value=None), \ + patch('services.chatbot.model.generate_content', side_effect=Exception("API Error")): result = await chat_with_gemini("Hello") @@ -248,9 +248,9 @@ async def test_full_chat_flow_with_game_detection(self, mock_environment_variabl } ] - with patch('backend.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ - patch('backend.chatbot.search_knowledge', return_value=mock_knowledge) as mock_search, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate: + with patch('services.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ + patch('services.chatbot.search_knowledge', return_value=mock_knowledge) as mock_search, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate: result = await chat_with_gemini("How do I craft a diamond sword?") @@ -274,8 +274,8 @@ async def test_chat_with_screenshot_analysis(self, mock_environment_variables, m mock_response = Mock() mock_response.text = "I can see you're playing Minecraft and need help with crafting" - with patch('backend.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ - patch('backend.chatbot.model.generate_content', return_value=mock_response) as mock_generate, \ + with patch('services.chatbot.detect_current_game', return_value="minecraft") as mock_detect, \ + patch('services.chatbot.model.generate_content', return_value=mock_response) as mock_generate, \ patch('PIL.Image.open') as mock_image_open, \ patch('base64.b64decode', return_value=b'decoded_image_data'): @@ -304,8 +304,8 @@ async def test_chat_with_empty_message(self, mock_environment_variables): mock_response = Mock() mock_response.text = "Empty message received" - with patch('backend.chatbot.detect_current_game', return_value=None), \ - patch('backend.chatbot.model.generate_content', return_value=mock_response): + with patch('services.chatbot.detect_current_game', return_value=None), \ + patch('services.chatbot.model.generate_content', return_value=mock_response): result = await chat_with_gemini("") @@ -319,8 +319,8 @@ async def test_chat_with_very_long_message(self, mock_environment_variables): mock_response = Mock() mock_response.text = "Long message processed" - with patch('backend.chatbot.detect_current_game', return_value=None), \ - patch('backend.chatbot.model.generate_content', return_value=mock_response): + with patch('services.chatbot.detect_current_game', return_value=None), \ + patch('services.chatbot.model.generate_content', return_value=mock_response): result = await chat_with_gemini(long_message) @@ -334,8 +334,8 @@ async def test_chat_with_special_characters(self, mock_environment_variables): mock_response = Mock() mock_response.text = "Special characters handled" - with patch('backend.chatbot.detect_current_game', return_value=None), \ - patch('backend.chatbot.model.generate_content', return_value=mock_response): + with patch('services.chatbot.detect_current_game', return_value=None), \ + patch('services.chatbot.model.generate_content', return_value=mock_response): result = await chat_with_gemini(special_message) @@ -351,10 +351,10 @@ async def test_chat_with_multiple_screenshot_keywords(self, mock_environment_var mock_screenshots = [(1, '2024-01-01T10:00:00', 'game.exe', 'Game', 'hash1')] mock_stats = {'total_screenshots': 1, 'applications': [('game.exe', 1)]} - with patch('backend.chatbot.detect_current_game', return_value=None), \ - patch('backend.chatbot.get_recent_screenshots', return_value=mock_screenshots), \ - patch('backend.chatbot.get_screenshot_stats', return_value=mock_stats), \ - patch('backend.chatbot.model.generate_content', return_value=mock_response): + with patch('services.chatbot.detect_current_game', return_value=None), \ + patch('services.chatbot.get_recent_screenshots', return_value=mock_screenshots), \ + patch('services.chatbot.get_screenshot_stats', return_value=mock_stats), \ + patch('services.chatbot.model.generate_content', return_value=mock_response): # Test various screenshot keywords keywords = ['screenshot', 'screen', 'capture', 'visual', 'see', 'show me'] diff --git a/tests/unit/test_game_detection.py b/tests/unit/test_game_detection.py index 4391061..bf35a0f 100644 --- a/tests/unit/test_game_detection.py +++ b/tests/unit/test_game_detection.py @@ -76,7 +76,7 @@ def test_detect_game_from_process_exception(self): @pytest.mark.unit def test_detect_game_from_screenshots_success(self, mock_screenshot_records): """Test successful game detection from screenshots.""" - with patch('backend.game_detection.get_recent_screenshots', return_value=mock_screenshot_records): + with patch('services.screenshot.get_recent_screenshots', return_value=mock_screenshot_records): detector = GameDetection() result = detector.detect_game_from_screenshots() @@ -89,7 +89,7 @@ def test_detect_game_from_screenshots_window_title_match(self): (1, '2024-01-01T10:00:00', 'unknown.exe', 'Minecraft Launcher', 'hash1'), ] - with patch('backend.game_detection.get_recent_screenshots', return_value=mock_screenshots): + with patch('services.screenshot.get_recent_screenshots', return_value=mock_screenshots): detector = GameDetection() result = detector.detect_game_from_screenshots() @@ -102,7 +102,7 @@ def test_detect_game_from_screenshots_no_match(self): (1, '2024-01-01T10:00:00', 'notepad.exe', 'Notepad', 'hash1'), ] - with patch('backend.game_detection.get_recent_screenshots', return_value=mock_screenshots): + with patch('services.screenshot.get_recent_screenshots', return_value=mock_screenshots): detector = GameDetection() result = detector.detect_game_from_screenshots() @@ -111,7 +111,7 @@ def test_detect_game_from_screenshots_no_match(self): @pytest.mark.unit def test_detect_game_from_screenshots_exception(self): """Test game detection from screenshots with exception.""" - with patch('backend.game_detection.get_recent_screenshots', side_effect=Exception("Screenshot error")): + with patch('services.screenshot.get_recent_screenshots', side_effect=Exception("Screenshot error")): detector = GameDetection() result = detector.detect_game_from_screenshots() @@ -358,7 +358,7 @@ def test_full_detection_workflow(self, mock_psutil_processes, mock_screenshot_re # Test with screenshot detection (both message and process fail) with patch.object(detector, 'detect_game_from_message', return_value=None), \ patch('psutil.process_iter', return_value=[]), \ - patch('backend.game_detection.get_recent_screenshots', return_value=mock_screenshot_records): + patch('services.screenshot.get_recent_screenshots', return_value=mock_screenshot_records): result = detector.detect_current_game("random question") assert result == 'minecraft' @@ -393,7 +393,7 @@ def test_game_mapping_lifecycle(self): mock_screenshots = [ (1, '2024-01-01T10:00:00', 'unknown.exe', 'Test Game Window', 'hash1'), ] - with patch('backend.game_detection.get_recent_screenshots', return_value=mock_screenshots): + with patch('services.screenshot.get_recent_screenshots', return_value=mock_screenshots): result = detector.detect_game_from_screenshots() assert result == 'test_integration_game' diff --git a/tests/unit/test_screenshot.py b/tests/unit/test_screenshot.py index 2cead82..9cb666d 100644 --- a/tests/unit/test_screenshot.py +++ b/tests/unit/test_screenshot.py @@ -40,8 +40,8 @@ class TestScreenshotCapture: @pytest.mark.unit def test_screenshot_capture_init(self, temp_dir, temp_db_path): """Test ScreenshotCapture initialization.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=b'test_key'): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=b'test_key'): capture = ScreenshotCapture(db_path=temp_db_path, interval=60) @@ -60,7 +60,7 @@ def test_get_or_create_key_existing(self, temp_dir): with open(key_file, "wb") as f: f.write(test_key) - with patch('backend.screenshot.Fernet') as mock_fernet: + with patch('services.screenshot.Fernet') as mock_fernet: # Change working directory so production code finds the key cwd = os.getcwd() try: @@ -73,8 +73,8 @@ def test_get_or_create_key_existing(self, temp_dir): @pytest.mark.unit def test_get_or_create_key_new(self, temp_dir): """Test creating new encryption key.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=b'new_key'): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=b'new_key'): # Change working directory so production code writes key into temp_dir cwd = os.getcwd() @@ -92,8 +92,8 @@ def test_get_or_create_key_new(self, temp_dir): @pytest.mark.unit def test_init_database(self, temp_dir, temp_db_path): """Test database initialization.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) # Verify database was created @@ -158,8 +158,8 @@ def test_capture_screenshot_error(self): @pytest.mark.unit def test_encrypt_decrypt_data(self, temp_dir, temp_db_path): """Test data encryption and decryption.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): mock_cipher = Mock() mock_cipher.encrypt.return_value = b'encrypted_data' mock_cipher.decrypt.return_value = b'original_data' @@ -179,8 +179,8 @@ def test_encrypt_decrypt_data(self, temp_dir, temp_db_path): @pytest.mark.unit def test_calculate_hash(self, temp_dir, temp_db_path): """Test hash calculation.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) test_data = b'test_data' @@ -194,8 +194,8 @@ def test_calculate_hash(self, temp_dir, temp_db_path): @pytest.mark.unit def test_save_screenshot_success(self, temp_dir, temp_db_path, mock_screenshot_data): """Test successful screenshot saving.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): mock_cipher = Mock() mock_cipher.encrypt.return_value = b'encrypted_data' mock_fernet.return_value = mock_cipher @@ -227,8 +227,8 @@ def test_save_screenshot_success(self, temp_dir, temp_db_path, mock_screenshot_d @pytest.mark.unit def test_save_screenshot_no_data(self, temp_dir, temp_db_path): """Test saving screenshot with no image data.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) window_info = {'application': 'test.exe', 'window_title': 'Test'} @@ -239,8 +239,8 @@ def test_save_screenshot_no_data(self, temp_dir, temp_db_path): @pytest.mark.unit def test_save_screenshot_error(self, temp_dir, temp_db_path, mock_screenshot_data): """Test saving screenshot with database error.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): mock_cipher = Mock() mock_cipher.encrypt.return_value = b'encrypted_data' @@ -258,8 +258,8 @@ def test_save_screenshot_error(self, temp_dir, temp_db_path, mock_screenshot_dat @pytest.mark.unit def test_capture_and_save(self, temp_dir, temp_db_path, mock_screenshot_data): """Test capture and save functionality.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ patch.object(ScreenshotCapture, '_get_active_window_info', return_value=mock_screenshot_data['window_info']), \ patch.object(ScreenshotCapture, '_capture_screenshot', @@ -283,8 +283,8 @@ def test_capture_and_save(self, temp_dir, temp_db_path, mock_screenshot_data): @pytest.mark.unit def test_start_capture(self, temp_dir, temp_db_path): """Test starting screenshot capture.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ patch('threading.Thread') as mock_thread: capture = ScreenshotCapture(db_path=temp_db_path) @@ -296,8 +296,8 @@ def test_start_capture(self, temp_dir, temp_db_path): @pytest.mark.unit def test_stop_capture(self, temp_dir, temp_db_path): """Test stopping screenshot capture.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) capture.running = True capture.thread = Mock() @@ -311,8 +311,8 @@ def test_stop_capture(self, temp_dir, temp_db_path): @pytest.mark.unit def test_get_screenshots_with_filters(self, temp_dir, temp_db_path, mock_screenshot_records): """Test getting screenshots with various filters.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) # Insert test data @@ -346,8 +346,8 @@ def test_get_screenshots_with_filters(self, temp_dir, temp_db_path, mock_screens @pytest.mark.unit def test_get_screenshot_data(self, temp_dir, temp_db_path): """Test getting screenshot data by ID.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): mock_cipher = Mock() mock_cipher.decrypt.return_value = b'decrypted_image_data' mock_fernet.return_value = mock_cipher @@ -376,8 +376,8 @@ def test_get_screenshot_data(self, temp_dir, temp_db_path): @pytest.mark.unit def test_get_stats(self, temp_dir, temp_db_path, mock_screenshot_records): """Test getting screenshot statistics.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) # Insert test data @@ -415,8 +415,8 @@ class TestScreenshotModuleFunctions: @pytest.mark.unit def test_start_screenshot_capture(self, temp_dir, temp_db_path): """Test starting screenshot capture via module function.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.screenshot_capture') as mock_capture: + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.screenshot_capture') as mock_capture: start_screenshot_capture(interval=45) @@ -426,14 +426,14 @@ def test_start_screenshot_capture(self, temp_dir, temp_db_path): @pytest.mark.unit def test_stop_screenshot_capture(self, temp_dir, temp_db_path): """Test stopping screenshot capture via module function.""" - with patch('backend.screenshot.screenshot_capture') as mock_capture: + with patch('services.screenshot.screenshot_capture') as mock_capture: stop_screenshot_capture() mock_capture.stop_capture.assert_called_once() @pytest.mark.unit def test_get_recent_screenshots(self, temp_dir, temp_db_path): """Test getting recent screenshots via module function.""" - with patch('backend.screenshot.screenshot_capture') as mock_capture: + with patch('services.screenshot.screenshot_capture') as mock_capture: mock_capture.get_screenshots.return_value = [('test', 'data')] result = get_recent_screenshots(limit=5, application='test.exe') @@ -444,7 +444,7 @@ def test_get_recent_screenshots(self, temp_dir, temp_db_path): @pytest.mark.unit def test_get_screenshot_by_id(self, temp_dir, temp_db_path): """Test getting screenshot by ID via module function.""" - with patch('backend.screenshot.screenshot_capture') as mock_capture: + with patch('services.screenshot.screenshot_capture') as mock_capture: mock_capture.get_screenshot_data.return_value = b'image_data' result = get_screenshot_by_id(123) @@ -455,7 +455,7 @@ def test_get_screenshot_by_id(self, temp_dir, temp_db_path): @pytest.mark.unit def test_get_screenshot_stats(self, temp_dir, temp_db_path): """Test getting screenshot stats via module function.""" - with patch('backend.screenshot.screenshot_capture') as mock_capture: + with patch('services.screenshot.screenshot_capture') as mock_capture: mock_stats = {'total_screenshots': 10, 'applications': []} mock_capture.get_stats.return_value = mock_stats @@ -514,8 +514,8 @@ class TestScreenshotIntegration: @pytest.mark.integration def test_full_screenshot_workflow(self, temp_dir, temp_db_path, mock_screenshot_data): """Test complete screenshot capture and retrieval workflow.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ patch.object(ScreenshotCapture, '_get_active_window_info', return_value=mock_screenshot_data['window_info']), \ patch.object(ScreenshotCapture, '_capture_screenshot', @@ -556,8 +556,8 @@ def test_full_screenshot_workflow(self, temp_dir, temp_db_path, mock_screenshot_ @pytest.mark.integration def test_screenshot_capture_threading(self, temp_dir, temp_db_path): """Test screenshot capture in threading environment.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()), \ patch.object(ScreenshotCapture, '_get_active_window_info', return_value={'application': 'test.exe', 'window_title': 'Test', 'pid': 123}), \ patch.object(ScreenshotCapture, '_capture_screenshot', @@ -585,8 +585,8 @@ class TestScreenshotEdgeCases: @pytest.mark.unit def test_screenshot_capture_with_invalid_image_data(self, temp_dir, temp_db_path): """Test screenshot capture with invalid image data.""" - with patch('backend.screenshot.Fernet'), \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet'), \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): capture = ScreenshotCapture(db_path=temp_db_path) window_info = {'application': 'test.exe', 'window_title': 'Test', 'pid': 123} @@ -602,8 +602,8 @@ def test_screenshot_capture_with_invalid_image_data(self, temp_dir, temp_db_path @pytest.mark.unit def test_concurrent_database_access(self, temp_dir, temp_db_path, mock_screenshot_data): """Test concurrent database access scenarios.""" - with patch('backend.screenshot.Fernet') as mock_fernet, \ - patch('backend.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): + with patch('services.screenshot.Fernet') as mock_fernet, \ + patch('services.screenshot.Fernet.generate_key', return_value=RealFernet.generate_key()): mock_cipher = Mock() mock_cipher.encrypt.return_value = b'encrypted_data' mock_fernet.return_value = mock_cipher diff --git a/tests/unit/test_vector_service.py b/tests/unit/test_vector_service.py index 3a05aad..42f1f1d 100644 --- a/tests/unit/test_vector_service.py +++ b/tests/unit/test_vector_service.py @@ -37,7 +37,7 @@ class TestVectorService: def test_vector_service_init(self, temp_vector_db_dir): """Test VectorService initialization.""" with patch('chromadb.PersistentClient') as mock_chroma, \ - patch('backend.vector_service.SentenceTransformer') as mock_transformer: + patch('services.vector_service.SentenceTransformer') as mock_transformer: service = VectorService(vector_db_dir=temp_vector_db_dir) @@ -64,7 +64,7 @@ def test_vector_service_init_chroma_error(self, temp_vector_db_dir): def test_vector_service_init_transformer_error(self, temp_vector_db_dir): """Test VectorService initialization with SentenceTransformer error.""" with patch('chromadb.PersistentClient'), \ - patch('backend.vector_service.SentenceTransformer', side_effect=Exception("Transformer error")): + patch('services.vector_service.SentenceTransformer', side_effect=Exception("Transformer error")): service = VectorService(vector_db_dir=temp_vector_db_dir) @@ -212,7 +212,7 @@ def test_chunk_text_single_sentence(self): @pytest.mark.unit def test_add_game_knowledge_success(self, mock_knowledge_data, mock_chroma_client, mock_chroma_collection, mock_embedding_model): """Test successful game knowledge addition.""" - with patch('backend.vector_service.process_game_knowledge', return_value=mock_knowledge_data) as mock_process: + with patch('services.vector_service.process_game_knowledge', return_value=mock_knowledge_data) as mock_process: service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = mock_embedding_model @@ -230,7 +230,7 @@ def test_add_game_knowledge_success(self, mock_knowledge_data, mock_chroma_clien @pytest.mark.unit def test_add_game_knowledge_no_client(self, mock_knowledge_data): """Test adding game knowledge without ChromaDB client.""" - with patch('backend.vector_service.process_game_knowledge', return_value=mock_knowledge_data): + with patch('services.vector_service.process_game_knowledge', return_value=mock_knowledge_data): service = VectorService() service.chroma_client = None @@ -241,7 +241,7 @@ def test_add_game_knowledge_no_client(self, mock_knowledge_data): @pytest.mark.unit def test_add_game_knowledge_no_model(self, mock_knowledge_data, mock_chroma_client): """Test adding game knowledge without embedding model.""" - with patch('backend.vector_service.process_game_knowledge', return_value=mock_knowledge_data): + with patch('services.vector_service.process_game_knowledge', return_value=mock_knowledge_data): service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = None @@ -255,7 +255,7 @@ def test_add_game_knowledge_empty_knowledge(self, mock_chroma_client, mock_embed """Test adding game knowledge with empty knowledge data.""" empty_knowledge = {'wiki': [], 'youtube': [], 'forum': []} - with patch('backend.vector_service.process_game_knowledge', return_value=empty_knowledge): + with patch('services.vector_service.process_game_knowledge', return_value=empty_knowledge): service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = mock_embedding_model @@ -268,7 +268,7 @@ def test_add_game_knowledge_empty_knowledge(self, mock_chroma_client, mock_embed @pytest.mark.unit def test_add_game_knowledge_error(self, mock_knowledge_data): """Test adding game knowledge with error.""" - with patch('backend.vector_service.process_game_knowledge', side_effect=Exception("Process error")): + with patch('services.vector_service.process_game_knowledge', side_effect=Exception("Process error")): service = VectorService() result = service.add_game_knowledge('test_game') @@ -554,7 +554,7 @@ class TestVectorServiceIntegration: @pytest.mark.integration def test_full_knowledge_workflow(self, mock_knowledge_data, mock_chroma_client, mock_embedding_model): """Test complete knowledge addition and search workflow.""" - with patch('backend.vector_service.process_game_knowledge', return_value=mock_knowledge_data): + with patch('services.vector_service.process_game_knowledge', return_value=mock_knowledge_data): service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = mock_embedding_model @@ -585,7 +585,7 @@ def test_full_knowledge_workflow(self, mock_knowledge_data, mock_chroma_client, @pytest.mark.integration def test_knowledge_lifecycle(self, mock_knowledge_data, mock_chroma_client, mock_embedding_model): """Test complete knowledge lifecycle (add, search, delete).""" - with patch('backend.vector_service.process_game_knowledge', return_value=mock_knowledge_data): + with patch('services.vector_service.process_game_knowledge', return_value=mock_knowledge_data): service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = mock_embedding_model @@ -657,7 +657,7 @@ def test_add_game_knowledge_with_malformed_data(self, mock_chroma_client, mock_e 'forum': [{'content': 'Valid content', 'title': 'Test'}] } - with patch('backend.vector_service.process_game_knowledge', return_value=malformed_knowledge): + with patch('services.vector_service.process_game_knowledge', return_value=malformed_knowledge): service = VectorService() service.chroma_client = mock_chroma_client service.embedding_model = mock_embedding_model diff --git a/uv.lock b/uv.lock index 66a2e7f..59ac8c5 100644 --- a/uv.lock +++ b/uv.lock @@ -817,6 +817,7 @@ dependencies = [ { name = "pandas" }, { name = "pillow" }, { name = "psutil" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, { name = "pytest" }, { name = "pywin32" }, { name = "requests" }, @@ -849,6 +850,7 @@ requires-dist = [ { name = "pandas", specifier = ">=2.0.0" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "psutil", specifier = ">=5.9.0" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'", specifier = ">=10.0" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.21.0" },