From 36bba89325e5116cae9b4ea47021770022d1f593 Mon Sep 17 00:00:00 2001 From: Husam Alshehadat Date: Fri, 27 Jun 2025 10:35:16 -0700 Subject: [PATCH] style: Fix formatting after sync --- communication_simulator_test.py | 11 ++- server.py | 2 +- simulator_tests/base_test.py | 18 +++- tests/test_uvx_support.py | 151 ++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 tests/test_uvx_support.py diff --git a/communication_simulator_test.py b/communication_simulator_test.py index e471b33..55b1a92 100644 --- a/communication_simulator_test.py +++ b/communication_simulator_test.py @@ -94,13 +94,14 @@ class CommunicationSimulator: self.quick_mode = quick_mode self.temp_dir = None self.server_process = None - self.python_path = self._get_python_path() # Configure logging first log_level = logging.DEBUG if verbose else logging.INFO logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") self.logger = logging.getLogger(__name__) + self.python_path = self._get_python_path() + # Import test registry from simulator_tests import TEST_REGISTRY @@ -133,8 +134,14 @@ class CommunicationSimulator: def _get_python_path(self) -> str: """Get the Python path for the virtual environment""" current_dir = os.getcwd() - venv_python = os.path.join(current_dir, "venv", "bin", "python") + # Try .venv first (modern convention) + venv_python = os.path.join(current_dir, ".venv", "bin", "python") + if os.path.exists(venv_python): + return venv_python + + # Try venv as fallback + venv_python = os.path.join(current_dir, "venv", "bin", "python") if os.path.exists(venv_python): return venv_python diff --git a/server.py b/server.py index ee1e37b..9e6fe31 100644 --- a/server.py +++ b/server.py @@ -32,7 +32,7 @@ from typing import Any, Optional # This is optional - environment variables can still be passed directly try: from dotenv import load_dotenv - + # Load environment variables from .env file in the script's directory # This ensures .env is loaded regardless of the current working directory script_dir = Path(__file__).parent diff --git a/simulator_tests/base_test.py b/simulator_tests/base_test.py index f6282e2..bbc0a75 100644 --- a/simulator_tests/base_test.py +++ b/simulator_tests/base_test.py @@ -21,21 +21,33 @@ class BaseSimulatorTest: self.verbose = verbose self.test_files = {} self.test_dir = None - self.python_path = self._get_python_path() - # Configure logging + # Configure logging first log_level = logging.DEBUG if verbose else logging.INFO logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") self.logger = logging.getLogger(self.__class__.__name__) + self.python_path = self._get_python_path() + def _get_python_path(self) -> str: """Get the Python path for the virtual environment""" current_dir = os.getcwd() - venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") + # Try .venv first (modern convention) + venv_python = os.path.join(current_dir, ".venv", "bin", "python") if os.path.exists(venv_python): return venv_python + # Try venv as fallback + venv_python = os.path.join(current_dir, "venv", "bin", "python") + if os.path.exists(venv_python): + return venv_python + + # Try .zen_venv as fallback + zen_venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") + if os.path.exists(zen_venv_python): + return zen_venv_python + # Fallback to system python if venv doesn't exist self.logger.warning("Virtual environment not found, using system python") return "python" diff --git a/tests/test_uvx_support.py b/tests/test_uvx_support.py new file mode 100644 index 0000000..419686b --- /dev/null +++ b/tests/test_uvx_support.py @@ -0,0 +1,151 @@ +""" +Test cases for uvx support and environment handling. +""" + +import os +import sys +from pathlib import Path +from unittest import mock + +import pytest + + +class TestUvxEnvironmentHandling: + """Test uvx-specific environment handling features.""" + + def test_dotenv_import_success(self): + """Test that dotenv is imported successfully when available.""" + # Mock successful dotenv import + with mock.patch.dict("sys.modules", {"dotenv": mock.MagicMock()}): + with mock.patch("dotenv.load_dotenv") as mock_load_dotenv: + # Re-import server module to trigger the import logic + if "server" in sys.modules: + del sys.modules["server"] + + import server # noqa: F401 + + # Should have called load_dotenv with the correct path + mock_load_dotenv.assert_called_once() + call_args = mock_load_dotenv.call_args + assert "dotenv_path" in call_args.kwargs + + def test_dotenv_import_failure_graceful_handling(self): + """Test that ImportError for dotenv is handled gracefully (uvx scenario).""" + # Mock only the dotenv import to fail + original_import = __builtins__["__import__"] + + def mock_import(name, *args, **kwargs): + if name == "dotenv": + raise ImportError("No module named 'dotenv'") + return original_import(name, *args, **kwargs) + + with mock.patch("builtins.__import__", side_effect=mock_import): + # This should not raise an exception when trying to import dotenv + try: + from dotenv import load_dotenv # noqa: F401 + + pytest.fail("Should have raised ImportError for dotenv") + except ImportError: + # Expected behavior - ImportError should be caught gracefully in server.py + pass + + def test_env_file_path_resolution(self): + """Test that .env file path is correctly resolved relative to server.py.""" + import server + + # Test that the server module correctly resolves .env path + script_dir = Path(server.__file__).parent + expected_env_file = script_dir / ".env" + + # The logic should create a path relative to server.py + assert expected_env_file.name == ".env" + assert expected_env_file.parent == script_dir + + def test_environment_variables_still_work_without_dotenv(self): + """Test that environment variables work even when dotenv is not available.""" + # Set a test environment variable + test_key = "TEST_ZEN_MCP_VAR" + test_value = "test_value_123" + + with mock.patch.dict(os.environ, {test_key: test_value}): + # Environment variable should still be accessible regardless of dotenv + assert os.getenv(test_key) == test_value + + def test_dotenv_graceful_fallback_behavior(self): + """Test the actual graceful fallback behavior in server module.""" + # Test that server module handles missing dotenv gracefully + # This is tested by the fact that the server can be imported even if dotenv fails + import server + + # If we can import server, the graceful handling works + assert hasattr(server, "run") + + # Test that environment variables still work + test_key = "TEST_FALLBACK_VAR" + test_value = "fallback_test_123" + + with mock.patch.dict(os.environ, {test_key: test_value}): + assert os.getenv(test_key) == test_value + + +class TestUvxProjectConfiguration: + """Test uvx-specific project configuration features.""" + + def test_pyproject_toml_has_required_uvx_fields(self): + """Test that pyproject.toml has all required fields for uvx support.""" + import tomllib + + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + assert pyproject_path.exists(), "pyproject.toml should exist" + + with open(pyproject_path, "rb") as f: + pyproject_data = tomllib.load(f) + + # Check required uvx fields + assert "project" in pyproject_data + project = pyproject_data["project"] + + # Essential fields for uvx + assert "name" in project + assert project["name"] == "zen-mcp-server" + assert "dependencies" in project + assert "requires-python" in project + + # Script entry point for uvx + assert "scripts" in project + assert "zen-mcp-server" in project["scripts"] + assert project["scripts"]["zen-mcp-server"] == "server:run" + + def test_pyproject_dependencies_match_requirements(self): + """Test that pyproject.toml dependencies align with requirements.txt.""" + import tomllib + + # Read pyproject.toml + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + with open(pyproject_path, "rb") as f: + pyproject_data = tomllib.load(f) + + pyproject_deps = set(pyproject_data["project"]["dependencies"]) + + # Read requirements.txt + requirements_path = Path(__file__).parent.parent / "requirements.txt" + if requirements_path.exists(): + # Note: We primarily validate pyproject.toml has core dependencies + # requirements.txt might have additional dev dependencies + + # Core dependencies should be present in both + core_packages = {"mcp", "openai", "google-genai", "pydantic", "python-dotenv"} + + for pkg in core_packages: + pyproject_has = any(pkg in dep for dep in pyproject_deps) + + assert pyproject_has, f"{pkg} should be in pyproject.toml dependencies" + # requirements.txt might have additional dev dependencies + + def test_uvx_entry_point_callable(self): + """Test that the uvx entry point (server:run) is callable.""" + import server + + # The entry point should reference a callable function + assert hasattr(server, "run"), "server module should have a 'run' function" + assert callable(server.run), "server.run should be callable"