development-hub/tests/test_paths.py

309 lines
11 KiB
Python

"""Tests for the PathResolver module."""
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from development_hub.paths import PathResolver, paths
from development_hub.settings import Settings
class TestPathResolver:
"""Test PathResolver path resolution."""
@pytest.fixture
def isolated_settings(self, tmp_path, monkeypatch):
"""Create isolated settings for testing."""
Settings._instance = None
PathResolver._instance = None
monkeypatch.setattr(Settings, "_settings_file", tmp_path / "settings.json")
monkeypatch.setattr(Settings, "_session_file", tmp_path / "session.json")
settings = Settings()
yield settings
Settings._instance = None
PathResolver._instance = None
def test_singleton_pattern(self):
"""PathResolver is a singleton."""
PathResolver._instance = None
resolver1 = PathResolver()
resolver2 = PathResolver()
assert resolver1 is resolver2
PathResolver._instance = None
def test_projects_root_from_settings(self, isolated_settings, tmp_path):
"""Projects root comes from settings."""
isolated_settings.project_search_paths = [str(tmp_path / "my-projects")]
resolver = PathResolver()
assert resolver.projects_root == tmp_path / "my-projects"
def test_projects_root_default(self, isolated_settings, tmp_path):
"""Projects root defaults to ~/Projects when not set."""
isolated_settings.project_search_paths = []
resolver = PathResolver()
assert resolver.projects_root == Path.home() / "Projects"
def test_docs_root_from_settings(self, isolated_settings, tmp_path):
"""Docs root comes from settings."""
isolated_settings.docs_mode = "standalone"
resolver = PathResolver()
docs_root = resolver.docs_root
assert "development-hub" in str(docs_root)
def test_project_docs_dir(self, isolated_settings, tmp_path):
"""Project docs dir returns docusaurus path."""
project_docs = tmp_path / "project-docs"
project_docs.mkdir()
isolated_settings.project_search_paths = [str(tmp_path)]
resolver = PathResolver()
assert resolver.project_docs_dir == project_docs
def test_project_docs_dir_none_when_not_exists(self, isolated_settings, tmp_path):
"""Project docs dir is None when not configured."""
isolated_settings.project_search_paths = [str(tmp_path)]
# No project-docs folder
resolver = PathResolver()
assert resolver.project_docs_dir is None
def test_progress_dir(self, isolated_settings):
"""Progress dir comes from settings."""
resolver = PathResolver()
progress = resolver.progress_dir
assert isinstance(progress, Path)
def test_build_script_when_exists(self, isolated_settings, tmp_path):
"""Build script returns path when it exists."""
project_docs = tmp_path / "project-docs"
scripts_dir = project_docs / "scripts"
scripts_dir.mkdir(parents=True)
build_script = scripts_dir / "build-public-docs.sh"
build_script.touch()
isolated_settings.project_search_paths = [str(tmp_path)]
resolver = PathResolver()
assert resolver.build_script == build_script
def test_build_script_none_when_not_exists(self, isolated_settings, tmp_path):
"""Build script is None when it doesn't exist."""
isolated_settings.project_search_paths = [str(tmp_path)]
resolver = PathResolver()
assert resolver.build_script is None
def test_project_docs_path(self, isolated_settings):
"""project_docs_path returns correct path for project."""
resolver = PathResolver()
path = resolver.project_docs_path("my-project")
assert path.name == "my-project"
assert "projects" in str(path)
def test_git_url_not_configured(self, isolated_settings):
"""git_url returns empty string when not configured."""
resolver = PathResolver()
assert resolver.git_url() == ""
assert resolver.git_url("owner", "repo") == ""
def test_git_url_configured(self, isolated_settings):
"""git_url returns correct URL when configured."""
isolated_settings.git_host_type = "github"
isolated_settings.git_host_url = "https://github.com"
isolated_settings.git_host_owner = "testowner"
resolver = PathResolver()
assert resolver.git_url() == "https://github.com/testowner"
assert resolver.git_url("testowner", "testrepo") == "https://github.com/testowner/testrepo"
def test_git_url_custom_owner(self, isolated_settings):
"""git_url can use custom owner."""
isolated_settings.git_host_type = "github"
isolated_settings.git_host_url = "https://github.com"
isolated_settings.git_host_owner = "default"
resolver = PathResolver()
assert resolver.git_url("custom", "repo") == "https://github.com/custom/repo"
def test_pages_url_not_configured(self, isolated_settings):
"""pages_url returns empty string when not configured."""
resolver = PathResolver()
assert resolver.pages_url() == ""
def test_pages_url_configured(self, isolated_settings):
"""pages_url returns correct URL when configured."""
isolated_settings.git_host_owner = "testowner"
isolated_settings.pages_url = "https://pages.example.com"
resolver = PathResolver()
assert "pages.example.com" in resolver.pages_url()
assert "testowner" in resolver.pages_url()
def test_is_docs_enabled(self, isolated_settings):
"""is_docs_enabled reflects settings."""
isolated_settings.docs_mode = "standalone"
resolver = PathResolver()
assert resolver.is_docs_enabled == True
def test_is_git_configured(self, isolated_settings):
"""is_git_configured reflects settings."""
resolver = PathResolver()
assert resolver.is_git_configured == False
isolated_settings.git_host_type = "github"
isolated_settings.git_host_url = "https://github.com"
isolated_settings.git_host_owner = "user"
assert resolver.is_git_configured == True
def test_effective_docs_mode(self, isolated_settings, tmp_path):
"""effective_docs_mode reflects settings."""
isolated_settings.project_search_paths = [str(tmp_path)]
isolated_settings.docs_mode = "auto"
resolver = PathResolver()
# No project-docs, so standalone
assert resolver.effective_docs_mode == "standalone"
# Create project-docs
(tmp_path / "project-docs").mkdir()
assert resolver.effective_docs_mode == "project-docs"
class TestCmdForgeAvailability:
"""Test CmdForge availability checks."""
@pytest.fixture
def isolated_settings(self, tmp_path, monkeypatch):
"""Create isolated settings for testing."""
Settings._instance = None
PathResolver._instance = None
monkeypatch.setattr(Settings, "_settings_file", tmp_path / "settings.json")
monkeypatch.setattr(Settings, "_session_file", tmp_path / "session.json")
settings = Settings()
# Use isolated paths to prevent finding real CmdForge
settings.project_search_paths = [str(tmp_path)]
yield settings
Settings._instance = None
PathResolver._instance = None
def test_cmdforge_path_explicit(self, isolated_settings, tmp_path):
"""CmdForge path from explicit setting."""
cmdforge_dir = tmp_path / "CmdForge"
cmdforge_dir.mkdir()
isolated_settings.cmdforge_path = cmdforge_dir
resolver = PathResolver()
assert resolver.cmdforge_path == cmdforge_dir
def test_cmdforge_executable_from_venv(self, isolated_settings, tmp_path):
"""CmdForge executable found in venv."""
cmdforge_dir = tmp_path / "CmdForge"
venv_bin = cmdforge_dir / ".venv" / "bin"
venv_bin.mkdir(parents=True)
cmdforge_exe = venv_bin / "cmdforge"
cmdforge_exe.touch()
isolated_settings.cmdforge_path = cmdforge_dir
resolver = PathResolver()
assert resolver.cmdforge_executable == cmdforge_exe
@patch("development_hub.paths.shutil.which")
def test_cmdforge_executable_from_path(self, mock_which, isolated_settings, tmp_path):
"""CmdForge executable found in PATH when not in explicit location."""
mock_which.return_value = "/usr/local/bin/cmdforge"
# Ensure no explicit cmdforge path is set
isolated_settings.cmdforge_path = None
resolver = PathResolver()
exe = resolver.cmdforge_executable
assert exe == Path("/usr/local/bin/cmdforge")
@patch("development_hub.paths.shutil.which")
def test_cmdforge_executable_not_found(self, mock_which, isolated_settings, tmp_path):
"""CmdForge executable None when not found."""
mock_which.return_value = None
# Ensure no explicit cmdforge path is set
isolated_settings.cmdforge_path = None
resolver = PathResolver()
assert resolver.cmdforge_executable is None
@patch("development_hub.paths.shutil.which")
def test_is_cmdforge_available_false(self, mock_which, isolated_settings, tmp_path):
"""is_cmdforge_available is False when not found."""
mock_which.return_value = None
isolated_settings.cmdforge_path = None
resolver = PathResolver()
assert resolver.is_cmdforge_available == False
@patch("development_hub.paths.shutil.which")
def test_is_cmdforge_available_true(self, mock_which, isolated_settings, tmp_path):
"""is_cmdforge_available is True when found in PATH."""
mock_which.return_value = "/usr/bin/cmdforge"
isolated_settings.cmdforge_path = None
resolver = PathResolver()
assert resolver.is_cmdforge_available == True
class TestGlobalPathsInstance:
"""Test the global paths singleton instance."""
def test_paths_is_path_resolver(self):
"""Global paths is a PathResolver instance."""
assert isinstance(paths, PathResolver)
def test_paths_has_expected_properties(self):
"""Global paths has expected properties."""
assert hasattr(paths, "projects_root")
assert hasattr(paths, "docs_root")
assert hasattr(paths, "project_docs_dir")
assert hasattr(paths, "progress_dir")
assert hasattr(paths, "build_script")
assert hasattr(paths, "cmdforge_path")
assert hasattr(paths, "cmdforge_executable")
assert hasattr(paths, "is_docs_enabled")
assert hasattr(paths, "is_cmdforge_available")
assert hasattr(paths, "is_git_configured")
def test_paths_has_expected_methods(self):
"""Global paths has expected methods."""
assert callable(paths.project_docs_path)
assert callable(paths.git_url)
assert callable(paths.pages_url)
if __name__ == "__main__":
pytest.main([__file__, "-v"])