"""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"])