"""Tests for shell_utils module. Focus areas: - Basic discovery when some shells exist. - No shells found scenario. - Deduplication of identical resolved paths. - Module-level SHELLS constant population at import. """ from __future__ import annotations import importlib import sys from types import ModuleType from typing import Callable import os import shutil def _reload_with_patches( monkeypatch, which_func: Callable[[str], str | None], exists_func: Callable[[str], bool], ) -> ModuleType: """Reload shell_utils with patched environment. Args: monkeypatch: pytest fixture for patching. which_func: replacement for shutil.which. exists_func: replacement for os.path.exists. Returns: Reloaded module object. """ monkeypatch.setattr(shutil, "which", which_func) monkeypatch.setattr(os.path, "exists", exists_func) # Ensure a clean import so SHELLS is recalculated. mod_name = "sme_external_terminal_mcp_server.shell_utils" sys.modules.pop(mod_name, None) return importlib.import_module(mod_name) def test_find_available_shells_basic(monkeypatch): """Basic case: subset of absolute shells exists and order preserved.""" allowed = { "/bin/bash", "/bin/sh", "/bin/zsh", } def fake_which(arg: str): # type: ignore[override] return arg if arg in allowed else None def fake_exists(path: str) -> bool: return path in allowed mod = _reload_with_patches(monkeypatch, fake_which, fake_exists) expected_prefix = ["/bin/bash", "/bin/sh", "/bin/zsh"] # Order should match candidate order filtered by availability. assert mod.SHELLS[: len(expected_prefix)] == expected_prefix assert len(mod.SHELLS) >= len(expected_prefix) def test_find_available_shells_none(monkeypatch): """When no shells are present, list is empty.""" def fake_which(arg: str): # type: ignore[override] return None def fake_exists(path: str) -> bool: # noqa: D401 return False mod = _reload_with_patches(monkeypatch, fake_which, fake_exists) assert mod.SHELLS == [] def test_deduplication(monkeypatch): """Duplicate resolved paths appear once; first occurrence kept.""" # Simulate only non-absolute candidate names (powershell.exe, cmd.exe) # both resolving to the same /bin/bash path while absolute paths are # treated as missing so they don't enter the list. duplicate_path = "/bin/bash" def fake_which(arg: str): # type: ignore[override] if arg in {"powershell.exe", "cmd.exe", duplicate_path}: return duplicate_path return None def fake_exists(path: str) -> bool: # absolute paths considered absent return False mod = _reload_with_patches(monkeypatch, fake_which, fake_exists) assert mod.SHELLS == [duplicate_path] def test_shells_constant_matches_function(monkeypatch): """SHELLS constant equals a fresh call to _find_available_shells.""" allowed = {"/bin/sh", "/bin/zsh"} def fake_which(arg: str): # type: ignore[override] return arg if arg in allowed else None def fake_exists(path: str) -> bool: return path in allowed mod = _reload_with_patches(monkeypatch, fake_which, fake_exists) # Recompute using internal function for comparison. recomputed = mod._find_available_shells() # type: ignore[attr-defined] assert mod.SHELLS == recomputed