forked from Rockachopa/Timmy-time-dashboard
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
273 lines
14 KiB
Python
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()
|