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" },