Skip to main content
Glama

MCP Git Server

by MementoRC
test_git_primitives.py21.8 kB
""" Test specifications for Git primitive operations. These tests define the behavioral requirements for atomic Git operations that serve as the foundation for higher-level Git functionality. CRITICAL: These tests are IMMUTABLE once complete. They define the interface and behavior that the git_primitives module MUST implement. DO NOT modify these tests to match implementation - implementation must satisfy these tests to prevent the LLM compliance issue. """ import subprocess from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Import the primitives we expect to be implemented # These imports will fail initially (RED phase) - that's expected! try: from mcp_server_git.primitives.git_primitives import ( GitCommandError, GitRepositoryError, GitValidationError, execute_git_command, format_git_error, get_commit_hash, get_current_branch, get_repository_status, get_staged_files, get_unstaged_files, get_untracked_files, is_git_repository, parse_git_log_output, parse_git_status_output, validate_repository_path, ) PRIMITIVES_AVAILABLE = True except ImportError: PRIMITIVES_AVAILABLE = False class TestExecuteGitCommand: """Test specifications for execute_git_command primitive.""" def test_should_execute_simple_git_commands(self): """execute_git_command should execute basic git commands.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock successful git command mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = "On branch main\nnothing to commit, working tree clean" mock_result.stderr = "" # ACT & ASSERT: Should execute command successfully with patch("subprocess.run", return_value=mock_result): result = execute_git_command("/tmp/repo", ["status", "--porcelain"]) assert result.success is True assert ( result.output == "On branch main\nnothing to commit, working tree clean" ) assert result.error is None assert result.return_code == 0 else: pytest.fail("execute_git_command not implemented - RED phase expected") def test_should_handle_git_command_failures(self): """execute_git_command should handle git command failures properly.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock failed git command mock_result = MagicMock() mock_result.returncode = 128 mock_result.stdout = "" mock_result.stderr = "fatal: not a git repository" # ACT & ASSERT: Should handle failure gracefully with patch("subprocess.run", return_value=mock_result): result = execute_git_command("/tmp/not-repo", ["status"]) assert result.success is False assert result.error == "fatal: not a git repository" assert result.return_code == 128 def test_should_validate_repository_path(self): """execute_git_command should validate repository path exists.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should raise error for non-existent path with pytest.raises(GitRepositoryError) as exc_info: execute_git_command("/does/not/exist", ["status"]) assert "repository path does not exist" in str(exc_info.value).lower() def test_should_validate_git_command_format(self): """execute_git_command should validate git command format.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should raise error for invalid commands with pytest.raises(GitCommandError) as exc_info: execute_git_command("/tmp/repo", []) # Empty command assert "invalid git command" in str(exc_info.value).lower() def test_should_handle_command_timeout(self): """execute_git_command should handle command timeouts.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock timeout with patch( "subprocess.run", side_effect=subprocess.TimeoutExpired("git", 30) ): # ACT & ASSERT: Should handle timeout gracefully result = execute_git_command("/tmp/repo", ["log"], timeout=30) assert result.success is False assert "timeout" in result.error.lower() class TestRepositoryValidation: """Test specifications for repository validation primitives.""" def test_is_git_repository_should_detect_valid_repos(self): """is_git_repository should correctly identify git repositories.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock valid git repository with ( patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.glob", return_value=[Path(".git")]), ): # ACT & ASSERT: Should identify as git repository assert is_git_repository("/tmp/valid-repo") is True def test_is_git_repository_should_reject_non_repos(self): """is_git_repository should reject non-git directories.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock non-git directory with ( patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.glob", return_value=[]), # No .git directory ): # ACT & ASSERT: Should reject as non-git repository assert is_git_repository("/tmp/not-repo") is False def test_validate_repository_path_should_accept_valid_paths(self): """validate_repository_path should accept valid repository paths.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock valid repository with ( patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.glob", return_value=[Path(".git")]), ): # ACT & ASSERT: Should validate successfully result = validate_repository_path("/tmp/valid-repo") assert result.is_valid is True assert result.absolute_path.is_absolute() def test_validate_repository_path_should_reject_invalid_paths(self): """validate_repository_path should reject invalid paths.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should raise validation error with pytest.raises(GitValidationError) as exc_info: validate_repository_path("/does/not/exist") assert "invalid repository path" in str(exc_info.value).lower() class TestRepositoryStatus: """Test specifications for repository status primitives.""" def test_get_repository_status_should_parse_clean_repo(self): """get_repository_status should correctly parse clean repository status.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock clean repository status mock_result = MagicMock() mock_result.success = True mock_result.output = "" # Empty porcelain output = clean with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get repository status status = get_repository_status("/tmp/repo") # ASSERT: Should indicate clean repository assert status.is_clean is True assert len(status.modified_files) == 0 assert len(status.untracked_files) == 0 assert len(status.staged_files) == 0 def test_get_repository_status_should_parse_dirty_repo(self): """get_repository_status should correctly parse dirty repository status.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock dirty repository status mock_result = MagicMock() mock_result.success = True mock_result.output = " M file1.py\n?? file2.py\nA file3.py" with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get repository status status = get_repository_status("/tmp/repo") # ASSERT: Should parse all file statuses correctly assert status.is_clean is False assert "file1.py" in status.modified_files assert "file2.py" in status.untracked_files assert "file3.py" in status.staged_files def test_get_staged_files_should_return_staged_files_only(self): """get_staged_files should return only staged files.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock git status output with staged files mock_result = MagicMock() mock_result.success = True mock_result.output = ( "A staged1.py\nM staged2.py\n M unstaged.py\n?? untracked.py" ) with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get staged files staged_files = get_staged_files("/tmp/repo") # ASSERT: Should return only staged files assert "staged1.py" in staged_files assert "staged2.py" in staged_files assert "unstaged.py" not in staged_files assert "untracked.py" not in staged_files def test_get_unstaged_files_should_return_unstaged_files_only(self): """get_unstaged_files should return only unstaged modified files.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock git status output with unstaged files mock_result = MagicMock() mock_result.success = True mock_result.output = ( "A staged.py\n M unstaged1.py\n D unstaged2.py\n?? untracked.py" ) with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get unstaged files unstaged_files = get_unstaged_files("/tmp/repo") # ASSERT: Should return only unstaged modified files assert "unstaged1.py" in unstaged_files assert "unstaged2.py" in unstaged_files assert "staged.py" not in unstaged_files assert "untracked.py" not in unstaged_files def test_get_untracked_files_should_return_untracked_files_only(self): """get_untracked_files should return only untracked files.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock git status output with untracked files mock_result = MagicMock() mock_result.success = True mock_result.output = ( "A staged.py\n M modified.py\n?? untracked1.py\n?? untracked2.py" ) with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get untracked files untracked_files = get_untracked_files("/tmp/repo") # ASSERT: Should return only untracked files assert "untracked1.py" in untracked_files assert "untracked2.py" in untracked_files assert "staged.py" not in untracked_files assert "modified.py" not in untracked_files class TestBranchOperations: """Test specifications for branch operation primitives.""" def test_get_current_branch_should_return_branch_name(self): """get_current_branch should return current branch name.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock git branch output mock_result = MagicMock() mock_result.success = True mock_result.output = "main" with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get current branch branch = get_current_branch("/tmp/repo") # ASSERT: Should return branch name assert branch == "main" def test_get_current_branch_should_handle_detached_head(self): """get_current_branch should handle detached HEAD state.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock detached HEAD state mock_result = MagicMock() mock_result.success = True mock_result.output = "HEAD detached at a1b2c3d" with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get current branch branch = get_current_branch("/tmp/repo") # ASSERT: Should return None or special indicator for detached HEAD assert branch is None or branch == "HEAD" class TestCommitOperations: """Test specifications for commit operation primitives.""" def test_get_commit_hash_should_return_current_commit(self): """get_commit_hash should return current commit hash.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock git rev-parse output mock_result = MagicMock() mock_result.success = True mock_result.output = "a1b2c3d4e5f6789012345678901234567890abcd" with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get commit hash commit_hash = get_commit_hash("/tmp/repo") # ASSERT: Should return full commit hash assert commit_hash == "a1b2c3d4e5f6789012345678901234567890abcd" assert len(commit_hash) == 40 def test_get_commit_hash_should_support_short_format(self): """get_commit_hash should support short hash format.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock short hash output mock_result = MagicMock() mock_result.success = True mock_result.output = "a1b2c3d" with patch( "mcp_server_git.primitives.git_primitives.execute_git_command", return_value=mock_result, ): # ACT: Get short commit hash commit_hash = get_commit_hash("/tmp/repo", short=True) # ASSERT: Should return short commit hash assert commit_hash == "a1b2c3d" assert len(commit_hash) == 7 class TestOutputParsing: """Test specifications for git output parsing primitives.""" def test_parse_git_status_output_should_parse_porcelain_format(self): """parse_git_status_output should correctly parse git status --porcelain output.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Sample git status --porcelain output status_output = """ M file1.py A file2.py D file3.py ?? file4.py R file5.py -> file6.py C file7.py -> file8.py""" # ACT: Parse status output parsed = parse_git_status_output(status_output) # ASSERT: Should correctly categorize all files assert "file1.py" in parsed.modified_files assert "file2.py" in parsed.added_files assert "file3.py" in parsed.deleted_files assert "file4.py" in parsed.untracked_files assert any("file5.py" in rename for rename in parsed.renamed_files) assert any("file7.py" in copy for copy in parsed.copied_files) def test_parse_git_log_output_should_parse_commit_entries(self): """parse_git_log_output should correctly parse git log output.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Sample git log output log_output = """commit a1b2c3d4e5f6789012345678901234567890abcd Author: John Doe <john@example.com> Date: Mon Dec 1 10:00:00 2023 +0000 feat: add new authentication system This commit adds a comprehensive authentication system with support for multiple providers. commit b2c3d4e5f6789012345678901234567890abcde1 Author: Jane Smith <jane@example.com> Date: Sun Nov 30 15:30:00 2023 +0000 fix: resolve memory leak in cache manager""" # ACT: Parse log output commits = parse_git_log_output(log_output) # ASSERT: Should parse all commit information assert len(commits) == 2 assert commits[0].hash == "a1b2c3d4e5f6789012345678901234567890abcd" assert commits[0].author_name == "John Doe" assert commits[0].author_email == "john@example.com" assert "feat: add new authentication system" in commits[0].message class TestErrorHandling: """Test specifications for error handling primitives.""" def test_format_git_error_should_create_readable_errors(self): """format_git_error should create human-readable error messages.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Raw git error raw_error = ( "fatal: not a git repository (or any of the parent directories): .git" ) command = ["git", "status"] repo_path = "/tmp/not-repo" # ACT: Format error formatted = format_git_error(raw_error, command, repo_path) # ASSERT: Should provide helpful error message assert "git repository" in formatted.message.lower() assert repo_path in formatted.context assert formatted.command == command assert formatted.suggestion is not None def test_git_command_error_should_provide_debug_info(self): """GitCommandError should provide comprehensive debug information.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should provide debug context try: raise GitCommandError( "Command failed", command=["git", "status"], repo_path="/tmp/repo" ) except GitCommandError as e: assert hasattr(e, "command") assert hasattr(e, "repo_path") assert hasattr(e, "return_code") assert hasattr(e, "stderr") def test_git_repository_error_should_indicate_repo_issues(self): """GitRepositoryError should clearly indicate repository-related issues.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should provide repository context try: raise GitRepositoryError( "Repository not found", repo_path="/tmp/missing" ) except GitRepositoryError as e: assert hasattr(e, "repo_path") assert hasattr(e, "suggested_action") def test_git_validation_error_should_provide_validation_context(self): """GitValidationError should provide detailed validation context.""" if PRIMITIVES_AVAILABLE: # ACT & ASSERT: Should provide validation details try: raise GitValidationError( "Invalid branch name", field="branch", value="invalid..name" ) except GitValidationError as e: assert hasattr(e, "field") assert hasattr(e, "value") assert hasattr(e, "validation_rule") # Integration tests to ensure primitives work together class TestPrimitiveIntegration: """Test specifications for primitive integration.""" def test_primitives_should_compose_cleanly(self): """Git primitives should compose together without conflicts.""" if PRIMITIVES_AVAILABLE: # ARRANGE: Mock repository repo_path = "/tmp/test-repo" with ( patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.glob", return_value=[Path(".git")]), ): # ACT: Use multiple primitives together is_valid = is_git_repository(repo_path) validated_path = validate_repository_path(repo_path) # ASSERT: Should work together seamlessly assert is_valid is True assert validated_path.is_valid is True def test_error_handling_should_be_consistent(self): """All primitives should handle errors consistently.""" if PRIMITIVES_AVAILABLE: # Test that all functions handle invalid repository paths consistently invalid_path = "/does/not/exist" # All these should raise appropriate errors with pytest.raises((GitRepositoryError, GitValidationError)): get_repository_status(invalid_path) with pytest.raises((GitRepositoryError, GitValidationError)): get_current_branch(invalid_path) with pytest.raises((GitRepositoryError, GitValidationError)): get_commit_hash(invalid_path) # Mark all tests that will initially fail (RED phase) pytestmark = [pytest.mark.unit, pytest.mark.primitives]

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MementoRC/mcp-git'

If you have feedback or need assistance with the MCP directory API, please join our Discord server