"""Tests for the Git Hand. Covers: - Destructive operation gating - Successful git operations (status, log) - Convenience wrappers - GitResult dataclass defaults - Info summary """ import pytest # --------------------------------------------------------------------------- # Destructive operation gating # --------------------------------------------------------------------------- def test_is_destructive_detects_force_push(): """Force-push should be flagged as destructive.""" from infrastructure.hands.git import GitHand hand = GitHand() assert hand._is_destructive("push --force origin main") is True assert hand._is_destructive("push -f origin main") is True def test_is_destructive_detects_hard_reset(): """Hard reset should be flagged as destructive.""" from infrastructure.hands.git import GitHand hand = GitHand() assert hand._is_destructive("reset --hard HEAD~1") is True def test_is_destructive_safe_ops(): """Safe operations should not be flagged.""" from infrastructure.hands.git import GitHand hand = GitHand() assert hand._is_destructive("status") is False assert hand._is_destructive("log --oneline -5") is False assert hand._is_destructive("push origin main") is False assert hand._is_destructive("commit -m 'test'") is False @pytest.mark.asyncio async def test_run_blocks_destructive_by_default(): """Destructive ops should be blocked unless allow_destructive=True.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.run("push --force origin main") assert result.success is False assert result.requires_confirmation is True assert "blocked" in result.error.lower() @pytest.mark.asyncio async def test_run_allows_destructive_with_flag(): """Destructive ops should execute when allow_destructive=True.""" from infrastructure.hands.git import GitHand # This will fail because there's no remote, but it should NOT be blocked hand = GitHand() result = await hand.run("push --force origin nonexistent-branch", allow_destructive=True) # It will fail (no remote), but it was NOT blocked by the gate assert result.requires_confirmation is False # --------------------------------------------------------------------------- # Successful operations # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_run_git_status(): """git status should succeed in a git repo.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.run("status --short") assert result.success is True assert result.latency_ms > 0 @pytest.mark.asyncio async def test_run_git_log(): """git log should succeed in a git repo.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.run("log --oneline -3") assert result.success is True assert result.output # should have at least one commit # --------------------------------------------------------------------------- # Convenience wrappers # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_status_wrapper(): """status() convenience wrapper should work.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.status() assert result.success is True assert result.operation == "git status --short" @pytest.mark.asyncio async def test_log_wrapper(): """log() convenience wrapper should work.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.log(count=3) assert result.success is True assert "git log" in result.operation @pytest.mark.asyncio async def test_diff_wrapper(): """diff() convenience wrapper should work.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.diff() assert result.success is True assert result.operation == "git diff" @pytest.mark.asyncio async def test_diff_staged_wrapper(): """diff(staged=True) should pass --cached.""" from infrastructure.hands.git import GitHand hand = GitHand() result = await hand.diff(staged=True) assert result.success is True assert result.operation == "git diff --cached" # --------------------------------------------------------------------------- # GitResult dataclass # --------------------------------------------------------------------------- def test_git_result_defaults(): """GitResult should have sensible defaults.""" from infrastructure.hands.git import GitResult r = GitResult(operation="git status", success=True) assert r.output == "" assert r.error == "" assert r.latency_ms == 0.0 assert r.requires_confirmation is False assert r.metadata == {} # --------------------------------------------------------------------------- # Info summary # --------------------------------------------------------------------------- def test_info_returns_summary(): """info() should return a dict with repo_dir and timeout.""" from infrastructure.hands.git import GitHand hand = GitHand() i = hand.info() assert "repo_dir" in i assert "default_timeout" in i # --------------------------------------------------------------------------- # Tool registration # --------------------------------------------------------------------------- def test_persona_hand_map(): """Forge and Helm should have both shell and git access.""" from infrastructure.hands.tools import get_local_hands_for_persona forge_hands = get_local_hands_for_persona("forge") assert "hand_shell" in forge_hands assert "hand_git" in forge_hands helm_hands = get_local_hands_for_persona("helm") assert "hand_shell" in helm_hands assert "hand_git" in helm_hands # Echo only gets git echo_hands = get_local_hands_for_persona("echo") assert "hand_git" in echo_hands assert "hand_shell" not in echo_hands # Quill gets neither quill_hands = get_local_hands_for_persona("quill") assert quill_hands == []