This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/self_coding/test_git_safety_errors.py
Claude 21846f3897 fix: disable gpg signing in test git fixtures and skip root-only permission test
Test fixtures that create temporary git repos now set commit.gpgsign=false
to avoid failures in environments with global commit signing configured.
The permission error test is skipped when running as root since file
permissions don't apply to the root user.

https://claude.ai/code/session_018u1fAx2GihSGctYS64tD4H
2026-02-27 01:52:47 +00:00

273 lines
14 KiB
Python

"""Error path tests for Git Safety Layer.
Tests timeout handling, git failures, merge conflicts, and edge cases.
"""
from __future__ import annotations
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest
from self_coding.git_safety import (
GitNotRepositoryError,
GitOperationError,
GitSafety,
)
@pytest.mark.asyncio
class TestGitSafetyErrors:
"""Git operation error handling."""
async def test_invalid_repo_path(self):
"""Should raise GitNotRepositoryError for non-repo."""
with tempfile.TemporaryDirectory() as tmpdir:
with pytest.raises(GitNotRepositoryError):
GitSafety(repo_path=tmpdir)
async def test_git_command_failure(self):
"""Should raise GitOperationError on git failure."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Try to checkout non-existent branch
with pytest.raises(GitOperationError):
await safety._run_git("checkout", "nonexistent-branch")
async def test_merge_conflict_detection(self):
"""Should handle merge conflicts gracefully."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
# Create initial file
(repo_path / "file.txt").write_text("original")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "branch", "-M", "main"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Create branch A with changes
await safety.create_branch("branch-a")
(repo_path / "file.txt").write_text("branch-a changes")
await safety.commit("Branch A changes")
# Go back to main, create branch B with conflicting changes
await safety._run_git("checkout", "main")
await safety.create_branch("branch-b")
(repo_path / "file.txt").write_text("branch-b changes")
await safety.commit("Branch B changes")
# Try to merge branch-a into branch-b (will conflict)
with pytest.raises(GitOperationError):
await safety._run_git("merge", "branch-a")
async def test_rollback_after_merge(self):
"""Should be able to rollback even after merge."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Initial commit
(repo_path / "file.txt").write_text("v1")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "v1"], cwd=repo_path, check=True, capture_output=True)
snapshot = await safety.snapshot(run_tests=False)
# Make changes and commit
(repo_path / "file.txt").write_text("v2")
await safety.commit("v2")
# Rollback
await safety.rollback(snapshot)
# Verify
content = (repo_path / "file.txt").read_text()
assert content == "v1"
async def test_snapshot_with_failing_tests(self):
"""Snapshot should capture failing test status."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
# Need an initial commit for HEAD to exist
(repo_path / "initial.txt").write_text("initial")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
# Create failing test
(repo_path / "test_fail.py").write_text("def test_fail(): assert False")
safety = GitSafety(
repo_path=repo_path,
test_command="python -m pytest test_fail.py -v",
)
snapshot = await safety.snapshot(run_tests=True)
assert snapshot.test_status is False
assert "FAILED" in snapshot.test_output or "failed" in snapshot.test_output.lower()
async def test_get_diff_between_commits(self):
"""Should get diff between any two commits."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Commit 1
(repo_path / "file.txt").write_text("version 1")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "v1"], cwd=repo_path, check=True, capture_output=True)
commit1 = await safety.get_current_commit()
# Commit 2
(repo_path / "file.txt").write_text("version 2")
await safety.commit("v2")
commit2 = await safety.get_current_commit()
# Get diff
diff = await safety.get_diff(commit1, commit2)
assert "version 1" in diff
assert "version 2" in diff
async def test_is_clean_with_untracked_files(self):
"""is_clean should return False with untracked files (they count as changes)."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
# Need an initial commit for HEAD to exist
(repo_path / "initial.txt").write_text("initial")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Verify clean state first
assert await safety.is_clean() is True
# Create untracked file
(repo_path / "untracked.txt").write_text("untracked")
# is_clean returns False when there are untracked files
# (git status --porcelain shows ?? for untracked)
assert await safety.is_clean() is False
async def test_empty_commit_allowed(self):
"""Should allow empty commits when requested."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
# Initial commit
(repo_path / "file.txt").write_text("content")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Empty commit
commit_hash = await safety.commit("Empty commit message", allow_empty=True)
assert len(commit_hash) == 40
async def test_modified_files_detection(self):
"""Should detect which files were modified."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path)
# Initial commits
(repo_path / "file1.txt").write_text("content1")
(repo_path / "file2.txt").write_text("content2")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
base_commit = await safety.get_current_commit()
# Modify only file1
(repo_path / "file1.txt").write_text("modified content")
await safety.commit("Modify file1")
# Get modified files
modified = await safety.get_modified_files(base_commit)
assert "file1.txt" in modified
assert "file2.txt" not in modified
async def test_branch_switching(self):
"""Should handle switching between branches."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir)
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo_path, check=True, capture_output=True)
# Initial commit on master (default branch name)
(repo_path / "main.txt").write_text("main branch content")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial"], cwd=repo_path, check=True, capture_output=True)
# Rename to main for consistency
subprocess.run(["git", "branch", "-M", "main"], cwd=repo_path, check=True, capture_output=True)
safety = GitSafety(repo_path=repo_path, main_branch="main")
# Create feature branch
await safety.create_branch("feature")
(repo_path / "feature.txt").write_text("feature content")
await safety.commit("Add feature")
# Switch back to main
await safety._run_git("checkout", "main")
# Verify main doesn't have feature.txt
assert not (repo_path / "feature.txt").exists()
# Switch to feature
await safety._run_git("checkout", "feature")
# Verify feature has feature.txt
assert (repo_path / "feature.txt").exists()