scripts-test-cli-tool-detector
#!/usr/bin/env python3 """
title: "CLI Tool Detector Tests" component_type: test version: "1.0.0" audience: contributor status: stable summary: "Unit tests for CLIToolDetector class" keywords: ['test', 'cli', 'detector', 'claude', 'codex', 'gemini'] tokens: ~300 created: 2026-01-28 updated: 2026-01-28
Unit tests for CLIToolDetector (J.13.4.1).
Tests the detection of installed LLM CLI tools and session path resolution.
Track: J.13 (Memory - Generic Session Export) Task: J.13.4.1 """
import os import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch, MagicMock
Add parent paths for imports
_test_dir = Path(file).resolve().parent _scripts_dir = _test_dir.parent _coditect_root = _scripts_dir.parent if str(_coditect_root) not in sys.path: sys.path.insert(0, str(_coditect_root)) if str(_scripts_dir) not in sys.path: sys.path.insert(0, str(_scripts_dir))
from core.cli_tool_detector import CLIToolDetector, ToolStatus, get_detector
class TestToolStatus(unittest.TestCase): """Tests for ToolStatus dataclass."""
def test_tool_status_installed(self):
"""Test ToolStatus with installed tool."""
status = ToolStatus(
name="claude",
installed=True,
version="2.1.19",
binary_path=Path("/usr/local/bin/claude")
)
self.assertTrue(status.installed)
self.assertEqual(status.version, "2.1.19")
self.assertEqual(status.name, "claude")
def test_tool_status_not_installed(self):
"""Test ToolStatus with non-installed tool."""
status = ToolStatus(name="unknown_tool", installed=False)
self.assertFalse(status.installed)
self.assertIsNone(status.version)
def test_tool_status_to_dict(self):
"""Test ToolStatus serialization."""
status = ToolStatus(
name="codex",
installed=True,
version="0.92.0",
binary_path=Path("/usr/local/bin/codex"),
session_paths=[Path("~/.codex/sessions")]
)
d = status.to_dict()
self.assertEqual(d["name"], "codex")
self.assertTrue(d["installed"])
self.assertEqual(d["version"], "0.92.0")
self.assertIn("binary_path", d)
class TestCLIToolDetector(unittest.TestCase): """Tests for CLIToolDetector class."""
def setUp(self):
"""Set up test fixtures."""
self.detector = CLIToolDetector()
def test_detector_has_cli_tools_defined(self):
"""Test that detector has CLI tools defined."""
tools = self.detector.CLI_TOOLS
self.assertIn("claude", tools)
self.assertIn("codex", tools)
self.assertIn("gemini", tools)
def test_detector_claude_config(self):
"""Test Claude CLI tool configuration."""
claude = self.detector.CLI_TOOLS["claude"]
self.assertEqual(claude["binary"], "claude")
self.assertIn("session_paths", claude)
self.assertIn("config_path", claude)
def test_detect_installed_tools_returns_dict(self):
"""Test that detect_installed_tools returns dict of ToolStatus."""
tools = self.detector.detect_installed_tools()
self.assertIsInstance(tools, dict)
# Should have entries for all known tools
for name in ["claude", "codex", "gemini"]:
self.assertIn(name, tools)
self.assertIsInstance(tools[name], ToolStatus)
def test_get_session_locations_returns_list(self):
"""Test that get_session_locations returns list of paths."""
locations = self.detector.get_session_locations("claude")
self.assertIsInstance(locations, list)
for loc in locations:
self.assertIsInstance(loc, Path)
def test_get_session_locations_unknown_llm(self):
"""Test get_session_locations with unknown LLM raises ValueError."""
with self.assertRaises(ValueError):
self.detector.get_session_locations("unknown_llm")
def test_is_tool_detected(self):
"""Test checking if a tool was detected."""
tools = self.detector.detect_installed_tools()
# Check if claude is in the detected tools
self.assertIn("claude", tools)
self.assertIsInstance(tools["claude"].installed, bool)
def test_get_detected_llm(self):
"""Test get_detected_llm returns string or None."""
llm = self.detector.get_detected_llm()
self.assertTrue(llm is None or isinstance(llm, str))
def test_to_dict(self):
"""Test detector serialization."""
d = self.detector.to_dict()
self.assertIn("detected_tools", d)
self.assertIn("active_llm", d)
self.assertIn("last_scan", d)
@patch('shutil.which')
def test_detect_with_mocked_which(self, mock_which):
"""Test detection with mocked shutil.which."""
mock_which.return_value = "/usr/local/bin/claude"
detector = CLIToolDetector()
# Reset cache to force re-detection
detector._detected_tools = {}
tools = detector.detect_installed_tools()
# Should have called which for each tool
self.assertTrue(mock_which.called)
class TestGetDetector(unittest.TestCase): """Tests for get_detector singleton function."""
def test_get_detector_returns_instance(self):
"""Test get_detector returns CLIToolDetector instance."""
detector = get_detector()
self.assertIsInstance(detector, CLIToolDetector)
def test_get_detector_singleton(self):
"""Test get_detector returns same instance."""
detector1 = get_detector()
detector2 = get_detector()
self.assertIs(detector1, detector2)
class TestEnvironmentOverrides(unittest.TestCase): """Tests for environment variable overrides."""
def test_env_override_parsing(self):
"""Test that session paths can include env vars."""
detector = CLIToolDetector()
gemini_config = detector.CLI_TOOLS["gemini"]
# Gemini should have an env var override option
session_paths = gemini_config.get("session_paths", [])
# Check that it can handle ${VAR} syntax
self.assertIsInstance(session_paths, list)
class TestCaching(unittest.TestCase): """Tests for detection caching."""
def test_detection_cached(self):
"""Test that detection results are cached."""
detector = CLIToolDetector()
# First call
tools1 = detector.detect_installed_tools()
# Second call should return same cached result
tools2 = detector.detect_installed_tools()
self.assertEqual(tools1, tools2)
def test_cache_force_refresh(self):
"""Test force_refresh bypasses cache."""
detector = CLIToolDetector()
# Populate cache
detector.detect_installed_tools()
# Force refresh
tools = detector.detect_installed_tools(force_refresh=True)
self.assertIsInstance(tools, dict)
if name == "main": unittest.main()