From 71a9fc848fb882f121cde6225b2a0b8bfa07b6a7 Mon Sep 17 00:00:00 2001 From: StillKnotKnown <192589389+StillKnotKnown@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:13:42 +0200 Subject: [PATCH] fix(windows): prevent pywintypes import errors before dependency validation (ACS-253) (#1057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(windows): prevent pywintypes import errors before dependency validation This fixes ACS-253 where Windows users on Python 3.12+ encounter ModuleNotFoundError for pywintypes when importing mcp.client.stdio. The issue occurred because graphiti_config was imported at module level in cli/utils.py, which triggered the import chain: graphiti_config → graphiti_core → real_ladybug → pywintypes This happened BEFORE validate_platform_dependencies() could check for pywin32 and provide helpful installation instructions. Changes: - cli/utils.py: Made graphiti_config import lazy (moved into validate_environment() function where it's actually used) - run.py: Added early validate_platform_dependencies() call before importing cli.main - runners/spec_runner.py: Added early validate_platform_dependencies() call before importing cli.utils Users now get a clear error message with installation instructions when pywin32 is missing, rather than a cryptic pywintypes import error. Refs: ACS-253 * test(windows): add comprehensive tests for dependency validator This adds test coverage for the ACS-253 fix preventing pywintypes import errors on Windows Python 3.12+. Test Coverage: - TestValidatePlatformDependencies (7 tests): - Windows + Python 3.12+ with pywin32 missing → exits with error - Windows + Python 3.12+ with pywin32 installed → continues - Windows + Python < 3.12 → skips validation - Linux/macOS → skips validation - Windows + Python 3.13+ → validates - Windows + Python 3.10 → skips validation - TestExitWithPywin32Error (3 tests): - Error message contains helpful instructions - Error message contains venv path - Error message contains Python executable - TestImportOrderPreventsEarlyFailure (3 tests): - validate_platform_dependencies doesn't import graphiti - cli/utils.py imports graphiti_config lazily - Entry points validate before CLI imports - TestCliUtilsFindSpec (4 tests): - Find spec by number prefix - Find spec by full name - Return None when not found - Require spec.md to exist - TestCliUtilsGetProjectDir (2 tests): - Return provided directory - Auto-detect from apps/backend directory - TestCliUtilsSetupEnvironment (2 tests): - Returns apps/backend directory - Adds to sys.path Total: 21 tests, all passing Refs: ACS-253 * refactor(tests): improve test robustness with AST and fix assertions Improvements made to test_dependency_validator.py: 1. AST-based function detection: Replace fragile string parsing with ast.parse() to find the first module-level function, avoiding false matches in docstrings or multi-line strings. 2. Fix setup_environment test: Remove unused temp_dir fixture and misleading assertion. Split into two focused tests: - test_setup_environment_returns_backend_dir: Verifies directory structure - test_setup_environment_adds_to_path: Verifies sys.path behavior 3. Remove redundant imports: Consolidate builtins imports to module-level, removing duplicate inner imports that shadow the top-level import. 4. Selective mock for pywintypes: Use selective_mock that returns MagicMock for pywintypes only, delegating all other imports to the original __import__ for more realistic test environment. 5. Strengthen venv path assertion: Require both "/path/to/venv" AND "Scripts" to be present in the error message, not just one or the other. 6. Add ast import: Add AST module import for robust parsing. All 21 tests pass. Refs: ACS-253 * fix(tests): address CodeQL and CodeRabbit review feedback - Remove unused Mock import from unittest.mock - Remove unused ast import from module level (kept local import in function) - Initialize validate_env_end_lineno before loop to prevent potential uninitialized variable use All 21 tests pass. Addresses review comments on PR #1057 Refs: ACS-253 * feat(windows): add dependency validation to all entry points for consistency Add validate_platform_dependencies() to all runner entry points for consistency with run.py and spec_runner.py. This provides defense-in-depth and ensures all entry points validate pywin32 on Windows Python 3.12+ before importing from cli.utils. Changes: - roadmap_runner.py: Added validate_platform_dependencies() before cli.utils import - ideation_runner.py: Added validate_platform_dependencies() before cli.utils import - insights_runner.py: Added validate_platform_dependencies() before cli.utils import - github/runner.py: Added validate_platform_dependencies() before cli.utils import - gitlab/runner.py: Added validate_platform_dependencies() before cli.utils import This completes the consistency improvements suggested by the Auto Claude PR Review. Refs: ACS-253 --------- Co-authored-by: StillKnotKnown --- apps/backend/cli/utils.py | 7 +- apps/backend/run.py | 6 + apps/backend/runners/github/runner.py | 6 + apps/backend/runners/gitlab/runner.py | 6 + apps/backend/runners/ideation_runner.py | 6 + apps/backend/runners/insights_runner.py | 6 + apps/backend/runners/roadmap_runner.py | 6 + apps/backend/runners/spec_runner.py | 6 + tests/test_dependency_validator.py | 455 ++++++++++++++++++++++++ 9 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 tests/test_dependency_validator.py diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index 03814689..f65b83c7 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -56,7 +56,9 @@ def import_dotenv(): # Load .env with helpful error if dependencies not installed load_dotenv = import_dotenv() -from graphiti_config import get_graphiti_status +# NOTE: graphiti_config is imported lazily in validate_environment() to avoid +# triggering graphiti_core -> real_ladybug -> pywintypes import chain before +# platform dependency validation can run. See ACS-253. from linear_integration import LinearManager from linear_updater import is_linear_enabled from spec.pipeline import get_specs_dir @@ -205,6 +207,9 @@ def validate_environment(spec_dir: Path) -> bool: print("Linear integration: DISABLED (set LINEAR_API_KEY to enable)") # Check Graphiti integration (optional but show status) + # Lazy import to avoid triggering pywintypes import before validation (ACS-253) + from graphiti_config import get_graphiti_status + graphiti_status = get_graphiti_status() if graphiti_status["available"]: print("Graphiti memory: ENABLED") diff --git a/apps/backend/run.py b/apps/backend/run.py index 25c24869..bd6c95f0 100644 --- a/apps/backend/run.py +++ b/apps/backend/run.py @@ -70,6 +70,12 @@ if sys.platform == "win32": if "_new_stream" in dir(): del _new_stream +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + from cli import main if __name__ == "__main__": diff --git a/apps/backend/runners/github/runner.py b/apps/backend/runners/github/runner.py index 103ffc72..67baf508 100644 --- a/apps/backend/runners/github/runner.py +++ b/apps/backend/runners/github/runner.py @@ -56,6 +56,12 @@ if sys.platform == "win32": # Add backend to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/apps/backend/runners/gitlab/runner.py b/apps/backend/runners/gitlab/runner.py index a24b284c..28826813 100644 --- a/apps/backend/runners/gitlab/runner.py +++ b/apps/backend/runners/gitlab/runner.py @@ -26,6 +26,12 @@ from pathlib import Path # Add backend to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/apps/backend/runners/ideation_runner.py b/apps/backend/runners/ideation_runner.py index 9b914456..76773c04 100644 --- a/apps/backend/runners/ideation_runner.py +++ b/apps/backend/runners/ideation_runner.py @@ -26,6 +26,12 @@ from pathlib import Path # Add auto-claude to path sys.path.insert(0, str(Path(__file__).parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/apps/backend/runners/insights_runner.py b/apps/backend/runners/insights_runner.py index bd4bf362..009b27c0 100644 --- a/apps/backend/runners/insights_runner.py +++ b/apps/backend/runners/insights_runner.py @@ -15,6 +15,12 @@ from pathlib import Path # Add auto-claude to path sys.path.insert(0, str(Path(__file__).parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/apps/backend/runners/roadmap_runner.py b/apps/backend/runners/roadmap_runner.py index 06625add..93b2d45e 100644 --- a/apps/backend/runners/roadmap_runner.py +++ b/apps/backend/runners/roadmap_runner.py @@ -20,6 +20,12 @@ from pathlib import Path # Add auto-claude to path sys.path.insert(0, str(Path(__file__).parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/apps/backend/runners/spec_runner.py b/apps/backend/runners/spec_runner.py index 39868daf..210c7120 100644 --- a/apps/backend/runners/spec_runner.py +++ b/apps/backend/runners/spec_runner.py @@ -81,6 +81,12 @@ if sys.platform == "win32": # Add auto-claude to path (parent of runners/) sys.path.insert(0, str(Path(__file__).parent.parent)) +# Validate platform-specific dependencies BEFORE any imports that might +# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253) +from core.dependency_validator import validate_platform_dependencies + +validate_platform_dependencies() + # Load .env file with centralized error handling from cli.utils import import_dotenv diff --git a/tests/test_dependency_validator.py b/tests/test_dependency_validator.py new file mode 100644 index 00000000..c0e42275 --- /dev/null +++ b/tests/test_dependency_validator.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +""" +Tests for dependency_validator module. + +Tests cover: +- Platform-specific dependency validation +- pywin32 validation on Windows Python 3.12+ +- Helpful error messages for missing dependencies +- No validation on non-Windows platforms +- No validation on Python < 3.12 +""" + +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +# Add apps/backend directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend")) + +from core.dependency_validator import validate_platform_dependencies, _exit_with_pywin32_error + + +# ============================================================================= +# TESTS FOR validate_platform_dependencies +# ============================================================================= + + +class TestValidatePlatformDependencies: + """Tests for validate_platform_dependencies function.""" + + def test_windows_python_312_with_pywin32_missing_exits(self): + """ + Windows + Python 3.12+ without pywin32 should exit with helpful message. + + This is the primary fix for ACS-253: ensure users get a clear error + message instead of a cryptic pywintypes import error. + """ + import builtins + + with patch("sys.platform", "win32"), \ + patch("sys.version_info", (3, 12, 0)), \ + patch("core.dependency_validator._exit_with_pywin32_error") as mock_exit: + + # Mock pywintypes import to raise ImportError + original_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == "pywintypes": + raise ImportError("No module named 'pywintypes'") + return original_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=mock_import): + validate_platform_dependencies() + + # Should have called the error exit function + mock_exit.assert_called_once() + + def test_windows_python_312_with_pywin32_installed_continues(self): + """Windows + Python 3.12+ with pywin32 installed should continue.""" + import builtins + + # Capture the original __import__ before any patching + original_import = builtins.__import__ + + def selective_mock(name, *args, **kwargs): + """Return mock for pywintypes, delegate everything else to original.""" + if name == "pywintypes": + return MagicMock() + return original_import(name, *args, **kwargs) + + with patch("sys.platform", "win32"), \ + patch("sys.version_info", (3, 12, 0)), \ + patch("builtins.__import__", side_effect=selective_mock): + # Should not raise SystemExit + validate_platform_dependencies() + + def test_windows_python_311_skips_validation(self): + """Windows + Python < 3.12 should skip pywin32 validation.""" + with patch("sys.platform", "win32"), \ + patch("sys.version_info", (3, 11, 0)), \ + patch("builtins.__import__") as mock_import: + # Even if pywintypes is not available, should not exit + mock_import.side_effect = ImportError("No module named 'pywintypes'") + + # Should not raise SystemExit + validate_platform_dependencies() + + def test_linux_skips_validation(self): + """Non-Windows platforms should skip pywin32 validation.""" + with patch("sys.platform", "linux"), \ + patch("sys.version_info", (3, 12, 0)), \ + patch("builtins.__import__") as mock_import: + # Even if pywintypes is not available, should not exit + mock_import.side_effect = ImportError("No module named 'pywintypes'") + + # Should not raise SystemExit + validate_platform_dependencies() + + def test_macos_skips_validation(self): + """macOS should skip pywin32 validation.""" + with patch("sys.platform", "darwin"), \ + patch("sys.version_info", (3, 12, 0)), \ + patch("builtins.__import__") as mock_import: + # Even if pywintypes is not available, should not exit + mock_import.side_effect = ImportError("No module named 'pywintypes'") + + # Should not raise SystemExit + validate_platform_dependencies() + + def test_windows_python_313_validates(self): + """Windows + Python 3.13+ should validate pywin32.""" + import builtins + + with patch("sys.platform", "win32"), \ + patch("sys.version_info", (3, 13, 0)), \ + patch("core.dependency_validator._exit_with_pywin32_error") as mock_exit: + + original_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == "pywintypes": + raise ImportError("No module named 'pywintypes'") + return original_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=mock_import): + validate_platform_dependencies() + + # Should have called the error exit function + mock_exit.assert_called_once() + + def test_windows_python_310_skips_validation(self): + """Windows + Python 3.10 should skip pywin32 validation.""" + with patch("sys.platform", "win32"), \ + patch("sys.version_info", (3, 10, 0)), \ + patch("builtins.__import__") as mock_import: + mock_import.side_effect = ImportError("No module named 'pywintypes'") + + # Should not raise SystemExit + validate_platform_dependencies() + + +# ============================================================================= +# TESTS FOR _exit_with_pywin32_error +# ============================================================================= + + +class TestExitWithPywin32Error: + """Tests for _exit_with_pywin32_error function.""" + + def test_exit_message_contains_helpful_instructions(self): + """Error message should include installation instructions.""" + with patch("sys.exit") as mock_exit: + _exit_with_pywin32_error() + + # Get the message passed to sys.exit + call_args = mock_exit.call_args[0][0] + message = str(call_args) + + # Verify helpful content + assert "pywin32" in message.lower() + assert "pip install" in message.lower() + assert "windows" in message.lower() + assert "python" in message.lower() + + def test_exit_message_contains_venv_path(self): + """Error message should include the virtual environment path.""" + with patch("sys.exit") as mock_exit, \ + patch("sys.prefix", "/path/to/venv"): + + _exit_with_pywin32_error() + + # Get the message passed to sys.exit + call_args = mock_exit.call_args[0][0] + message = str(call_args) + + # Should reference the full venv Scripts/activate path + assert "/path/to/venv" in message + assert "Scripts" in message + + def test_exit_message_contains_python_executable(self): + """Error message should include the current Python executable.""" + with patch("sys.exit") as mock_exit, \ + patch("sys.executable", "/usr/bin/python3.12"): + + _exit_with_pywin32_error() + + # Get the message passed to sys.exit + call_args = mock_exit.call_args[0][0] + message = str(call_args) + + # Should reference the current Python executable + assert "python" in message.lower() + + +# ============================================================================= +# TESTS FOR IMPORT ORDER (ACS-253 FIX) +# ============================================================================= + + +class TestImportOrderPreventsEarlyFailure: + """ + Tests that validate the ACS-253 fix: dependency validation happens + BEFORE graphiti imports that trigger pywintypes. + """ + + def test_validate_platform_dependencies_does_not_import_graphiti(self): + """ + validate_platform_dependencies should not trigger graphiti imports. + + This test ensures the fix for ACS-253 is working: the dependency + validator runs early and doesn't import modules that would trigger + the graphiti_core -> real_ladybug -> pywintypes import chain. + """ + import builtins + + # Track imports made during validation + imported_modules = set() + original_import = builtins.__import__ + + def tracking_import(name, *args, **kwargs): + imported_modules.add(name) + return original_import(name, *args, **kwargs) + + # Use non-Windows to avoid import issues + with patch("builtins.__import__", side_effect=tracking_import), \ + patch("sys.platform", "linux"), \ + patch("sys.version_info", (3, 11, 0)): + + validate_platform_dependencies() + + # Verify graphiti-related modules were NOT imported + assert "graphiti_core" not in imported_modules + assert "real_ladybug" not in imported_modules + assert "graphiti_config" not in imported_modules + + def test_cli_utils_lazy_import_of_graphiti_config(self): + """ + cli/utils.py directly imports graphiti_config lazily in validate_environment(). + + The fix ensures that graphiti_config is NOT imported at the module level + in cli/utils.py (line 59). Instead, it's imported lazily inside the + validate_environment() function where it's actually used. + + Note: graphiti_config may still be imported transitively through other + modules imported by cli.utils (e.g., linear_integration, spec.pipeline). + The key fix is that the DIRECT import from cli/utils.py is lazy. + """ + import ast + + # Read cli/utils.py to verify the import is NOT at module level + backend_dir = Path(__file__).parent.parent / "apps" / "backend" + utils_py = backend_dir / "cli" / "utils.py" + utils_content = utils_py.read_text() + + # Parse the file with AST to find the first function definition + tree = ast.parse(utils_content) + + # Find the line number of the first top-level function + first_function_lineno = None + for node in tree.body: + if isinstance(node, ast.FunctionDef): + first_function_lineno = node.lineno + break + elif isinstance(node, (ast.AsyncFunctionDef, ast.ClassDef)): + # Skip async functions and classes, find first regular function + continue + + assert first_function_lineno is not None, "Could not find first function in cli/utils.py" + + # Check module-level imports (before the first function) + lines = utils_content.split("\n") + module_level_imports = "\n".join(lines[:first_function_lineno]) + + assert "from graphiti_config import" not in module_level_imports, \ + "graphiti_config should not be imported at module level in cli/utils.py" + + # Verify that graphiti_config IS imported inside validate_environment() + validate_env_lineno = None + validate_env_end_lineno = len(lines) # Initialize to end of file + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == "validate_environment": + validate_env_lineno = node.lineno + # Find the end of the function (next top-level node or end of file) + node_index = tree.body.index(node) + if node_index + 1 < len(tree.body): + next_node = tree.body[node_index + 1] + validate_env_end_lineno = next_node.lineno + break + + assert validate_env_lineno is not None, "Could not find validate_environment function" + + # Look for the import within the function's body + validate_env_block = "\n".join(lines[validate_env_lineno - 1:validate_env_end_lineno]) + assert "from graphiti_config import get_graphiti_status" in validate_env_block, \ + "graphiti_config should be imported inside validate_environment()" + + def test_entry_points_validate_before_cli_imports(self): + """ + Entry points (run.py, spec_runner.py) should validate dependencies + BEFORE importing cli modules that might trigger graphiti imports. + """ + # Read entry point files and verify the order + backend_dir = Path(__file__).parent.parent / "apps" / "backend" + + # Check run.py + run_py = backend_dir / "run.py" + run_content = run_py.read_text() + + # Verify validate_platform_dependencies is imported and called + assert "validate_platform_dependencies" in run_content, \ + "run.py should import validate_platform_dependencies" + + # Find the position of validation call and cli import + validation_pos = run_content.find("validate_platform_dependencies()") + cli_import_pos = run_content.find("from cli import main") + + assert validation_pos > 0, "run.py should call validate_platform_dependencies" + assert cli_import_pos > 0, "run.py should import cli.main" + assert validation_pos < cli_import_pos, \ + "run.py should validate dependencies BEFORE importing cli.main" + + # Check spec_runner.py + spec_runner_py = backend_dir / "runners" / "spec_runner.py" + spec_runner_content = spec_runner_py.read_text() + + assert "validate_platform_dependencies" in spec_runner_content, \ + "spec_runner.py should import validate_platform_dependencies" + + # Find positions + validation_pos_spec = spec_runner_content.find("validate_platform_dependencies()") + cli_utils_import_pos = spec_runner_content.find("from cli.utils import") + + assert validation_pos_spec > 0, "spec_runner.py should call validate_platform_dependencies" + assert cli_utils_import_pos > 0, "spec_runner.py should import cli.utils" + assert validation_pos_spec < cli_utils_import_pos, \ + "spec_runner.py should validate dependencies BEFORE importing cli.utils" + + +# ============================================================================= +# TESTS FOR CLI UTILS FUNCTIONS +# ============================================================================= + + +class TestCliUtilsFindSpec: + """Tests for find_spec function in cli/utils.py.""" + + def test_find_spec_by_number(self, temp_dir): + """Find spec by number prefix.""" + from cli.utils import find_spec + + # Create spec directory + specs_dir = temp_dir / ".auto-claude" / "specs" + specs_dir.mkdir(parents=True) + spec_dir = specs_dir / "001-test-feature" + spec_dir.mkdir() + (spec_dir / "spec.md").write_text("# Test Spec") + + result = find_spec(temp_dir, "001") + assert result == spec_dir + + def test_find_spec_by_full_name(self, temp_dir): + """Find spec by full directory name.""" + from cli.utils import find_spec + + specs_dir = temp_dir / ".auto-claude" / "specs" + specs_dir.mkdir(parents=True) + spec_dir = specs_dir / "001-test-feature" + spec_dir.mkdir() + (spec_dir / "spec.md").write_text("# Test Spec") + + result = find_spec(temp_dir, "001-test-feature") + assert result == spec_dir + + def test_find_spec_returns_none_when_not_found(self, temp_dir): + """Return None when spec doesn't exist.""" + from cli.utils import find_spec + + result = find_spec(temp_dir, "999") + assert result is None + + def test_find_spec_requires_spec_md(self, temp_dir): + """Require spec.md to exist in the directory.""" + from cli.utils import find_spec + + specs_dir = temp_dir / ".auto-claude" / "specs" + specs_dir.mkdir(parents=True) + spec_dir = specs_dir / "001-test-feature" + spec_dir.mkdir() + # Don't create spec.md + + result = find_spec(temp_dir, "001") + assert result is None + + +class TestCliUtilsGetProjectDir: + """Tests for get_project_dir function.""" + + def test_get_project_dir_returns_provided_dir(self, temp_dir): + """Return provided directory when given.""" + from cli.utils import get_project_dir + + result = get_project_dir(temp_dir) + assert result == temp_dir + + def test_get_project_dir_auto_detects_backend(self, temp_dir): + """Auto-detect when running from apps/backend directory.""" + from cli.utils import get_project_dir + + # Create apps/backend structure + backend_dir = temp_dir / "apps" / "backend" + backend_dir.mkdir(parents=True) + (backend_dir / "run.py").write_text("# run.py") + + # Change to backend directory + import os + original_cwd = os.getcwd() + try: + os.chdir(backend_dir) + result = get_project_dir(None) + # Should go up 2 levels from backend to project root + assert result == temp_dir + finally: + os.chdir(original_cwd) + + +class TestCliUtilsSetupEnvironment: + """Tests for setup_environment function.""" + + def test_setup_environment_returns_backend_dir(self): + """ + setup_environment returns the script directory (apps/backend). + + Note: The function uses Path(__file__).parent.parent.resolve() which + always points to the actual cli/utils.py location (apps/backend), + not a temporary directory. This test verifies the expected behavior. + """ + from cli.utils import setup_environment + + # Setup environment + script_dir = setup_environment() + + # Verify script_dir is the apps/backend directory + assert script_dir.name == "backend" + assert script_dir.parent.name == "apps" + + def test_setup_environment_adds_to_path(self): + """Add script directory to sys.path.""" + from cli.utils import setup_environment + + script_dir = setup_environment() + + # Verify script_dir is in sys.path + assert str(script_dir) in sys.path