mirror of
https://github.com/NickCrew/Claude-Cortex.git
synced 2026-09-14 20:17:11 +08:00
c99f5e445d
Add complete MCP (Model Context Protocol) server management with CLI, TUI, and Python API interfaces. Features: - Core MCP module for server discovery and validation - CLI commands: list, show, docs, test, diagnose, snippet - TUI view (key 7) with visual dashboard and keyboard shortcuts - Curated documentation for 6 popular MCP servers - Read-only approach respecting Claude Desktop as source of truth - Cross-platform support (macOS, Linux, Windows) Test Coverage: - 50 tests for core MCP module (100% passing) - 33 tests for TUI MCP view (82.94% coverage) - Full integration with existing test suite Documentation: - Complete user guide (MCP_MANAGEMENT.md) - Implementation summary with architecture details - Module README and usage examples - TUI keyboard reference Test Coverage Planning: - Created 11 GitHub issues for reaching 80% test coverage - Automation script for issue creation - Comprehensive test coverage plan with timeline - Issue template for future test coverage work Files Added: - claude_ctx_py/core/mcp.py - Core MCP module - claude_ctx_py/tui_mcp.py - TUI MCP view - tests/unit/test_mcp.py - Core module tests - tests/unit/test_tui_mcp.py - TUI view tests - docs/MCP_*.md - MCP documentation - ~/.claude/mcp/docs/*.md - Server documentation - CREATE_THESE_ISSUES.md - GitHub issue content - scripts/create_test_coverage_issues.sh - Issue automation Files Modified: - claude_ctx_py/cli.py - Added MCP subcommand - claude_ctx_py/tui.py - Integrated MCP view - claude_ctx_py/core/__init__.py - Exported MCP functions - README.md - Added MCP management section Related Issues: #1-#11
519 lines
20 KiB
Python
519 lines
20 KiB
Python
"""Tests for semantic versioning utilities."""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from claude_ctx_py import versioner
|
|
|
|
|
|
class TestParseVersion:
|
|
"""Tests for parse_version function."""
|
|
|
|
def test_parse_version_valid_simple(self):
|
|
"""Test parsing simple semantic version."""
|
|
major, minor, patch = versioner.parse_version("1.2.3")
|
|
|
|
assert major == 1
|
|
assert minor == 2
|
|
assert patch == 3
|
|
|
|
def test_parse_version_with_v_prefix(self):
|
|
"""Test parsing version with 'v' prefix."""
|
|
major, minor, patch = versioner.parse_version("v2.0.1")
|
|
|
|
assert major == 2
|
|
assert minor == 0
|
|
assert patch == 1
|
|
|
|
def test_parse_version_zeros(self):
|
|
"""Test parsing version with zeros."""
|
|
major, minor, patch = versioner.parse_version("0.0.0")
|
|
|
|
assert major == 0
|
|
assert minor == 0
|
|
assert patch == 0
|
|
|
|
def test_parse_version_large_numbers(self):
|
|
"""Test parsing version with large numbers."""
|
|
major, minor, patch = versioner.parse_version("10.25.100")
|
|
|
|
assert major == 10
|
|
assert minor == 25
|
|
assert patch == 100
|
|
|
|
@pytest.mark.parametrize("invalid_version", [
|
|
"1.2", # Missing patch
|
|
"1", # Missing minor and patch
|
|
"1.2.3.4", # Too many components
|
|
"a.b.c", # Non-numeric
|
|
"1.2.3-beta", # Pre-release tag not supported
|
|
"1.2.3+build", # Build metadata not supported
|
|
"", # Empty string
|
|
"v", # Just prefix
|
|
])
|
|
def test_parse_version_invalid_formats(self, invalid_version):
|
|
"""Test parsing invalid version formats raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid semantic version"):
|
|
versioner.parse_version(invalid_version)
|
|
|
|
|
|
class TestCheckCompatibility:
|
|
"""Tests for check_compatibility function."""
|
|
|
|
# Exact version tests
|
|
def test_check_compatibility_exact_match(self):
|
|
"""Test exact version match."""
|
|
assert versioner.check_compatibility("1.2.3", "1.2.3") is True
|
|
|
|
def test_check_compatibility_exact_mismatch(self):
|
|
"""Test exact version mismatch."""
|
|
assert versioner.check_compatibility("1.2.3", "1.2.4") is False
|
|
assert versioner.check_compatibility("1.2.3", "1.3.3") is False
|
|
assert versioner.check_compatibility("1.2.3", "2.2.3") is False
|
|
|
|
# Caret (^) tests
|
|
def test_check_compatibility_caret_patch_update(self):
|
|
"""Test caret allows patch updates."""
|
|
assert versioner.check_compatibility("^1.2.0", "1.2.5") is True
|
|
|
|
def test_check_compatibility_caret_minor_update(self):
|
|
"""Test caret allows minor updates."""
|
|
assert versioner.check_compatibility("^1.2.0", "1.3.0") is True
|
|
assert versioner.check_compatibility("^1.2.0", "1.10.5") is True
|
|
|
|
def test_check_compatibility_caret_major_mismatch(self):
|
|
"""Test caret blocks major version changes."""
|
|
assert versioner.check_compatibility("^1.2.0", "2.0.0") is False
|
|
assert versioner.check_compatibility("^1.2.0", "0.9.9") is False
|
|
|
|
def test_check_compatibility_caret_exact_version(self):
|
|
"""Test caret accepts exact version."""
|
|
assert versioner.check_compatibility("^1.2.0", "1.2.0") is True
|
|
|
|
def test_check_compatibility_caret_downgrade(self):
|
|
"""Test caret blocks downgrades."""
|
|
assert versioner.check_compatibility("^1.2.5", "1.2.3") is False
|
|
|
|
# Tilde (~) tests
|
|
def test_check_compatibility_tilde_patch_update(self):
|
|
"""Test tilde allows patch updates."""
|
|
assert versioner.check_compatibility("~1.2.0", "1.2.5") is True
|
|
|
|
def test_check_compatibility_tilde_minor_blocked(self):
|
|
"""Test tilde blocks minor updates."""
|
|
assert versioner.check_compatibility("~1.2.0", "1.3.0") is False
|
|
|
|
def test_check_compatibility_tilde_major_blocked(self):
|
|
"""Test tilde blocks major updates."""
|
|
assert versioner.check_compatibility("~1.2.0", "2.2.0") is False
|
|
|
|
def test_check_compatibility_tilde_exact_version(self):
|
|
"""Test tilde accepts exact version."""
|
|
assert versioner.check_compatibility("~1.2.0", "1.2.0") is True
|
|
|
|
def test_check_compatibility_tilde_downgrade(self):
|
|
"""Test tilde blocks downgrades."""
|
|
assert versioner.check_compatibility("~1.2.5", "1.2.3") is False
|
|
|
|
# Minimum (>=) tests
|
|
def test_check_compatibility_minimum_exact(self):
|
|
"""Test minimum accepts exact version."""
|
|
assert versioner.check_compatibility(">=1.2.0", "1.2.0") is True
|
|
|
|
def test_check_compatibility_minimum_higher(self):
|
|
"""Test minimum accepts higher versions."""
|
|
assert versioner.check_compatibility(">=1.2.0", "1.2.5") is True
|
|
assert versioner.check_compatibility(">=1.2.0", "1.3.0") is True
|
|
assert versioner.check_compatibility(">=1.2.0", "2.0.0") is True
|
|
|
|
def test_check_compatibility_minimum_lower(self):
|
|
"""Test minimum blocks lower versions."""
|
|
assert versioner.check_compatibility(">=1.2.0", "1.1.9") is False
|
|
assert versioner.check_compatibility(">=1.2.0", "0.9.9") is False
|
|
|
|
# Latest tests
|
|
def test_check_compatibility_latest_always_matches(self):
|
|
"""Test that 'latest' always matches."""
|
|
assert versioner.check_compatibility("latest", "0.0.1") is True
|
|
assert versioner.check_compatibility("latest", "999.999.999") is True
|
|
assert versioner.check_compatibility("LATEST", "1.0.0") is True
|
|
|
|
# Invalid version tests
|
|
def test_check_compatibility_invalid_available_version(self):
|
|
"""Test compatibility check with invalid available version."""
|
|
assert versioner.check_compatibility("1.2.0", "invalid") is False
|
|
assert versioner.check_compatibility("^1.2.0", "not-a-version") is False
|
|
|
|
def test_check_compatibility_invalid_requirement(self):
|
|
"""Test compatibility check with invalid requirement."""
|
|
assert versioner.check_compatibility("invalid", "1.2.0") is False
|
|
|
|
|
|
class TestGetSkillVersions:
|
|
"""Tests for get_skill_versions function."""
|
|
|
|
def test_get_skill_versions_multiple_versions(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test getting all versions of a skill."""
|
|
versions = versioner.get_skill_versions("pdf", temp_claude_dir)
|
|
|
|
assert versions == ["2.1.0", "2.0.0", "1.5.3", "1.0.0"]
|
|
|
|
def test_get_skill_versions_no_versions(self, temp_claude_dir):
|
|
"""Test getting versions for nonexistent skill."""
|
|
versions = versioner.get_skill_versions("nonexistent", temp_claude_dir)
|
|
|
|
assert versions == []
|
|
|
|
def test_get_skill_versions_skills_dir_not_exists(self, tmp_path):
|
|
"""Test getting versions when skills directory doesn't exist."""
|
|
non_existent_dir = tmp_path / "nonexistent"
|
|
versions = versioner.get_skill_versions("pdf", non_existent_dir)
|
|
|
|
assert versions == []
|
|
|
|
def test_get_skill_versions_ignores_invalid_versions(self, temp_claude_dir):
|
|
"""Test that invalid version strings are skipped."""
|
|
skills_dir = temp_claude_dir / "skills"
|
|
|
|
# Create valid and invalid version directories
|
|
(skills_dir / "test@1.0.0").mkdir()
|
|
(skills_dir / "test@invalid").mkdir()
|
|
(skills_dir / "test@2.0.0").mkdir()
|
|
(skills_dir / "test@").mkdir()
|
|
|
|
versions = versioner.get_skill_versions("test", temp_claude_dir)
|
|
|
|
assert versions == ["2.0.0", "1.0.0"]
|
|
|
|
def test_get_skill_versions_ignores_files(self, temp_claude_dir):
|
|
"""Test that non-directory entries are ignored."""
|
|
skills_dir = temp_claude_dir / "skills"
|
|
|
|
# Create directories
|
|
(skills_dir / "test@1.0.0").mkdir()
|
|
|
|
# Create a file (should be ignored)
|
|
(skills_dir / "test@2.0.0").touch()
|
|
|
|
versions = versioner.get_skill_versions("test", temp_claude_dir)
|
|
|
|
assert versions == ["1.0.0"]
|
|
|
|
|
|
class TestGetLatestVersion:
|
|
"""Tests for get_latest_version function."""
|
|
|
|
def test_get_latest_version_with_versions(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test getting latest version when versions exist."""
|
|
latest = versioner.get_latest_version("pdf", temp_claude_dir)
|
|
|
|
assert latest == "2.1.0"
|
|
|
|
def test_get_latest_version_no_versions(self, temp_claude_dir):
|
|
"""Test getting latest version when no versions exist."""
|
|
latest = versioner.get_latest_version("nonexistent", temp_claude_dir)
|
|
|
|
assert latest == "latest"
|
|
|
|
|
|
class TestValidateVersionRequirement:
|
|
"""Tests for validate_version_requirement function."""
|
|
|
|
@pytest.mark.parametrize("valid_req", [
|
|
"1.2.3",
|
|
"0.0.0",
|
|
"^1.2.3",
|
|
"~1.2.3",
|
|
">=1.2.3",
|
|
"latest",
|
|
"LATEST",
|
|
])
|
|
def test_validate_version_requirement_valid(self, valid_req):
|
|
"""Test validation of valid version requirements."""
|
|
assert versioner.validate_version_requirement(valid_req) is True
|
|
|
|
@pytest.mark.parametrize("invalid_req", [
|
|
"invalid",
|
|
"1.2",
|
|
"a.b.c",
|
|
"^invalid",
|
|
"~not-a-version",
|
|
">=bad",
|
|
"",
|
|
"1.2.3-beta",
|
|
])
|
|
def test_validate_version_requirement_invalid(self, invalid_req):
|
|
"""Test validation of invalid version requirements."""
|
|
assert versioner.validate_version_requirement(invalid_req) is False
|
|
|
|
|
|
class TestParseSkillWithVersion:
|
|
"""Tests for parse_skill_with_version function."""
|
|
|
|
def test_parse_skill_with_version_exact(self):
|
|
"""Test parsing skill with exact version."""
|
|
name, version = versioner.parse_skill_with_version("pdf@1.2.3")
|
|
|
|
assert name == "pdf"
|
|
assert version == "1.2.3"
|
|
|
|
def test_parse_skill_with_version_caret(self):
|
|
"""Test parsing skill with caret version."""
|
|
name, version = versioner.parse_skill_with_version("pdf@^1.2.0")
|
|
|
|
assert name == "pdf"
|
|
assert version == "^1.2.0"
|
|
|
|
def test_parse_skill_with_version_tilde(self):
|
|
"""Test parsing skill with tilde version."""
|
|
name, version = versioner.parse_skill_with_version("pdf@~1.2.0")
|
|
|
|
assert name == "pdf"
|
|
assert version == "~1.2.0"
|
|
|
|
def test_parse_skill_with_version_minimum(self):
|
|
"""Test parsing skill with minimum version."""
|
|
name, version = versioner.parse_skill_with_version("pdf@>=1.2.0")
|
|
|
|
assert name == "pdf"
|
|
assert version == ">=1.2.0"
|
|
|
|
def test_parse_skill_with_version_latest(self):
|
|
"""Test parsing skill with latest."""
|
|
name, version = versioner.parse_skill_with_version("pdf@latest")
|
|
|
|
assert name == "pdf"
|
|
assert version == "latest"
|
|
|
|
def test_parse_skill_with_version_no_version(self):
|
|
"""Test parsing skill without version defaults to latest."""
|
|
name, version = versioner.parse_skill_with_version("pdf")
|
|
|
|
assert name == "pdf"
|
|
assert version == "latest"
|
|
|
|
def test_parse_skill_with_version_whitespace(self):
|
|
"""Test parsing skill with whitespace is trimmed."""
|
|
name, version = versioner.parse_skill_with_version(" pdf @ 1.2.3 ")
|
|
|
|
assert name == "pdf"
|
|
assert version == "1.2.3"
|
|
|
|
def test_parse_skill_with_version_invalid_version(self):
|
|
"""Test parsing skill with invalid version raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid semantic version"):
|
|
versioner.parse_skill_with_version("pdf@invalid")
|
|
|
|
|
|
class TestFormatSkillWithVersion:
|
|
"""Tests for format_skill_with_version function."""
|
|
|
|
def test_format_skill_with_version_exact(self):
|
|
"""Test formatting skill with exact version."""
|
|
result = versioner.format_skill_with_version("pdf", "1.2.3")
|
|
|
|
assert result == "pdf@1.2.3"
|
|
|
|
def test_format_skill_with_version_caret(self):
|
|
"""Test formatting skill with caret version."""
|
|
result = versioner.format_skill_with_version("pdf", "^1.2.0")
|
|
|
|
assert result == "pdf@^1.2.0"
|
|
|
|
def test_format_skill_with_version_latest(self):
|
|
"""Test formatting skill with latest."""
|
|
result = versioner.format_skill_with_version("pdf", "latest")
|
|
|
|
assert result == "pdf@latest"
|
|
|
|
def test_format_skill_with_version_empty_version(self):
|
|
"""Test formatting skill with empty version defaults to latest."""
|
|
result = versioner.format_skill_with_version("pdf", "")
|
|
|
|
assert result == "pdf@latest"
|
|
|
|
def test_format_skill_with_version_empty_name(self):
|
|
"""Test formatting with empty skill name raises ValueError."""
|
|
with pytest.raises(ValueError, match="Skill name cannot be empty"):
|
|
versioner.format_skill_with_version("", "1.2.3")
|
|
|
|
def test_format_skill_with_version_invalid_version(self):
|
|
"""Test formatting with invalid version raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid semantic version"):
|
|
versioner.format_skill_with_version("pdf", "invalid")
|
|
|
|
|
|
class TestResolveVersion:
|
|
"""Tests for resolve_version function."""
|
|
|
|
def test_resolve_version_latest(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving 'latest' returns highest version."""
|
|
resolved = versioner.resolve_version("pdf", "latest", temp_claude_dir)
|
|
|
|
assert resolved == "2.1.0"
|
|
|
|
def test_resolve_version_exact_match(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving exact version match."""
|
|
resolved = versioner.resolve_version("pdf", "1.5.3", temp_claude_dir)
|
|
|
|
assert resolved == "1.5.3"
|
|
|
|
def test_resolve_version_exact_no_match(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving exact version with no match."""
|
|
resolved = versioner.resolve_version("pdf", "9.9.9", temp_claude_dir)
|
|
|
|
assert resolved is None
|
|
|
|
def test_resolve_version_caret(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving caret version requirement."""
|
|
resolved = versioner.resolve_version("pdf", "^1.0.0", temp_claude_dir)
|
|
|
|
# Should return highest 1.x.x version (1.5.3)
|
|
assert resolved == "1.5.3"
|
|
|
|
def test_resolve_version_tilde(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving tilde version requirement."""
|
|
resolved = versioner.resolve_version("pdf", "~2.0.0", temp_claude_dir)
|
|
|
|
# Should return highest 2.0.x version (2.0.0 is the only one)
|
|
assert resolved == "2.0.0"
|
|
|
|
def test_resolve_version_minimum(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving minimum version requirement."""
|
|
resolved = versioner.resolve_version("pdf", ">=1.5.0", temp_claude_dir)
|
|
|
|
# Should return highest version >= 1.5.0 (2.1.0)
|
|
assert resolved == "2.1.0"
|
|
|
|
def test_resolve_version_no_versions(self, temp_claude_dir):
|
|
"""Test resolving version when no versions exist."""
|
|
resolved = versioner.resolve_version("nonexistent", "1.0.0", temp_claude_dir)
|
|
|
|
assert resolved is None
|
|
|
|
def test_resolve_version_no_compatible(self, temp_claude_dir, skill_versions_dir):
|
|
"""Test resolving when no compatible version exists."""
|
|
resolved = versioner.resolve_version("pdf", "^3.0.0", temp_claude_dir)
|
|
|
|
assert resolved is None
|
|
|
|
|
|
class TestLoadSkillMetadata:
|
|
"""Tests for load_skill_metadata function."""
|
|
|
|
def test_load_skill_metadata_yaml(self, temp_claude_dir):
|
|
"""Test loading metadata from .yaml file."""
|
|
skill_dir = temp_claude_dir / "skills" / "test@1.0.0"
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
metadata_file = skill_dir / "skill.yaml"
|
|
metadata_file.write_text(
|
|
"name: test\nversion: 1.0.0\ndescription: Test skill\n",
|
|
encoding="utf-8"
|
|
)
|
|
|
|
metadata = versioner.load_skill_metadata(skill_dir)
|
|
|
|
assert metadata["name"] == "test"
|
|
assert metadata["version"] == "1.0.0"
|
|
assert metadata["description"] == "Test skill"
|
|
|
|
def test_load_skill_metadata_yml(self, temp_claude_dir):
|
|
"""Test loading metadata from .yml file."""
|
|
skill_dir = temp_claude_dir / "skills" / "test@1.0.0"
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
metadata_file = skill_dir / "skill.yml"
|
|
metadata_file.write_text(
|
|
"name: test\nversion: 1.0.0\n",
|
|
encoding="utf-8"
|
|
)
|
|
|
|
metadata = versioner.load_skill_metadata(skill_dir)
|
|
|
|
assert metadata["name"] == "test"
|
|
assert metadata["version"] == "1.0.0"
|
|
|
|
def test_load_skill_metadata_no_yaml(self, temp_claude_dir):
|
|
"""Test loading metadata when no YAML file exists."""
|
|
skill_dir = temp_claude_dir / "skills" / "test@1.0.0"
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
metadata = versioner.load_skill_metadata(skill_dir)
|
|
|
|
assert metadata == {}
|
|
|
|
def test_load_skill_metadata_invalid_yaml(self, temp_claude_dir):
|
|
"""Test loading metadata with invalid YAML."""
|
|
skill_dir = temp_claude_dir / "skills" / "test@1.0.0"
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
metadata_file = skill_dir / "skill.yaml"
|
|
metadata_file.write_text("invalid: yaml: content: [", encoding="utf-8")
|
|
|
|
metadata = versioner.load_skill_metadata(skill_dir)
|
|
|
|
assert metadata == {}
|
|
|
|
def test_load_skill_metadata_not_dict(self, temp_claude_dir):
|
|
"""Test loading metadata that's not a dictionary."""
|
|
skill_dir = temp_claude_dir / "skills" / "test@1.0.0"
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
metadata_file = skill_dir / "skill.yaml"
|
|
metadata_file.write_text("- item1\n- item2\n", encoding="utf-8")
|
|
|
|
metadata = versioner.load_skill_metadata(skill_dir)
|
|
|
|
assert metadata == {}
|
|
|
|
|
|
class TestCompareVersions:
|
|
"""Tests for compare_versions function."""
|
|
|
|
def test_compare_versions_equal(self):
|
|
"""Test comparing equal versions."""
|
|
assert versioner.compare_versions("1.2.3", "1.2.3") == 0
|
|
|
|
def test_compare_versions_less_than(self):
|
|
"""Test comparing when first is less than second."""
|
|
assert versioner.compare_versions("1.2.3", "1.2.4") == -1
|
|
assert versioner.compare_versions("1.2.3", "1.3.0") == -1
|
|
assert versioner.compare_versions("1.2.3", "2.0.0") == -1
|
|
|
|
def test_compare_versions_greater_than(self):
|
|
"""Test comparing when first is greater than second."""
|
|
assert versioner.compare_versions("1.2.4", "1.2.3") == 1
|
|
assert versioner.compare_versions("1.3.0", "1.2.3") == 1
|
|
assert versioner.compare_versions("2.0.0", "1.2.3") == 1
|
|
|
|
def test_compare_versions_major_difference(self):
|
|
"""Test major version differences."""
|
|
assert versioner.compare_versions("2.0.0", "1.9.9") == 1
|
|
assert versioner.compare_versions("1.0.0", "2.0.0") == -1
|
|
|
|
def test_compare_versions_minor_difference(self):
|
|
"""Test minor version differences."""
|
|
assert versioner.compare_versions("1.2.0", "1.1.9") == 1
|
|
assert versioner.compare_versions("1.1.0", "1.2.0") == -1
|
|
|
|
def test_compare_versions_patch_difference(self):
|
|
"""Test patch version differences."""
|
|
assert versioner.compare_versions("1.2.3", "1.2.2") == 1
|
|
assert versioner.compare_versions("1.2.2", "1.2.3") == -1
|
|
|
|
def test_compare_versions_with_v_prefix(self):
|
|
"""Test comparing versions with v prefix."""
|
|
assert versioner.compare_versions("v1.2.3", "v1.2.3") == 0
|
|
assert versioner.compare_versions("v1.2.3", "1.2.4") == -1
|
|
|
|
def test_compare_versions_invalid_first(self):
|
|
"""Test comparing with invalid first version."""
|
|
with pytest.raises(ValueError):
|
|
versioner.compare_versions("invalid", "1.2.3")
|
|
|
|
def test_compare_versions_invalid_second(self):
|
|
"""Test comparing with invalid second version."""
|
|
with pytest.raises(ValueError):
|
|
versioner.compare_versions("1.2.3", "invalid")
|