585 lines
20 KiB
Python
585 lines
20 KiB
Python
"""
|
|
Regression tests for GitHub PR creation after GitLab support was added.
|
|
|
|
This test suite verifies that:
|
|
1. GitHub remotes are still detected correctly
|
|
2. push_and_create_pr correctly routes to create_pull_request for GitHub
|
|
3. gh CLI is still invoked with correct arguments
|
|
4. No regressions in existing GitHub PR functionality
|
|
5. Provider field is correctly set to "github"
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Add apps/backend directory to path for imports
|
|
_backend_dir = Path(__file__).parent.parent / "apps" / "backend"
|
|
if str(_backend_dir) not in sys.path:
|
|
sys.path.insert(0, str(_backend_dir))
|
|
|
|
from core.git_provider import detect_git_provider
|
|
from worktree import PullRequestResult, WorktreeInfo, WorktreeManager
|
|
|
|
|
|
class TestGitHubProviderDetection:
|
|
"""Test that GitHub remotes are still detected correctly."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolate_git_env(self):
|
|
"""Clear GIT_* environment variables to prevent worktree interference."""
|
|
# Store original values
|
|
git_vars = {k: v for k, v in os.environ.items() if k.startswith('GIT_')}
|
|
# Clear GIT environment variables
|
|
for k in list(git_vars.keys()):
|
|
del os.environ[k]
|
|
yield
|
|
# Restore original values
|
|
for k, v in git_vars.items():
|
|
os.environ[k] = v
|
|
|
|
def test_github_https_detection(self, tmp_path):
|
|
"""Test GitHub HTTPS URL detection."""
|
|
repo_path = tmp_path / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
# Initialize git repo with GitHub remote
|
|
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "remote", "add", "origin", "https://github.com/user/repo.git"],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
provider = detect_git_provider(repo_path)
|
|
assert provider == "github", f"Expected 'github', got '{provider}'"
|
|
|
|
def test_github_ssh_detection(self, tmp_path):
|
|
"""Test GitHub SSH URL detection."""
|
|
repo_path = tmp_path / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
# Initialize git repo with GitHub remote
|
|
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "remote", "add", "origin", "git@github.com:user/repo.git"],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
provider = detect_git_provider(repo_path)
|
|
assert provider == "github", f"Expected 'github', got '{provider}'"
|
|
|
|
def test_github_enterprise_detection(self, tmp_path):
|
|
"""Test GitHub Enterprise URL detection."""
|
|
repo_path = tmp_path / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
# Initialize git repo with GitHub Enterprise remote
|
|
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"remote",
|
|
"add",
|
|
"origin",
|
|
"https://github.company.com/user/repo.git",
|
|
],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
provider = detect_git_provider(repo_path)
|
|
assert provider == "github", f"Expected 'github', got '{provider}'"
|
|
|
|
|
|
class TestGitHubPRRouting:
|
|
"""Test that push_and_create_pr correctly routes to create_pull_request for GitHub."""
|
|
|
|
def test_github_routing_to_create_pull_request(
|
|
self, worktree_manager, temp_project_dir
|
|
):
|
|
"""Test that GitHub remotes route to create_pull_request."""
|
|
spec_name = "test-spec"
|
|
|
|
# Mock push_branch to succeed
|
|
mock_push_result = {
|
|
"success": True,
|
|
"remote": "origin",
|
|
"branch": f"auto-claude/{spec_name}",
|
|
}
|
|
|
|
# Mock PR creation result
|
|
mock_pr_result = PullRequestResult(
|
|
success=True,
|
|
pr_url="https://github.com/user/repo/pull/123",
|
|
already_exists=False,
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(
|
|
worktree_manager, "push_branch", return_value=mock_push_result
|
|
),
|
|
# Patch on the module object directly to handle importlib shim loading
|
|
patch.object(worktree_module, "detect_git_provider", return_value="github"),
|
|
patch.object(
|
|
worktree_manager, "create_pull_request", return_value=mock_pr_result
|
|
) as mock_create_pr,
|
|
):
|
|
result = worktree_manager.push_and_create_pr(
|
|
spec_name=spec_name,
|
|
target_branch="main",
|
|
title="Test PR",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify create_pull_request was called
|
|
mock_create_pr.assert_called_once_with(
|
|
spec_name=spec_name,
|
|
target_branch="main",
|
|
title="Test PR",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify result
|
|
assert result["success"] is True
|
|
assert result["pushed"] is True
|
|
assert result["provider"] == "github"
|
|
assert result["pr_url"] == "https://github.com/user/repo/pull/123"
|
|
|
|
def test_github_provider_field_set_correctly(
|
|
self, worktree_manager, temp_project_dir
|
|
):
|
|
"""Test that provider field is set to 'github' in result."""
|
|
spec_name = "test-spec"
|
|
|
|
# Mock push_branch to succeed
|
|
mock_push_result = {
|
|
"success": True,
|
|
"remote": "origin",
|
|
"branch": f"auto-claude/{spec_name}",
|
|
}
|
|
|
|
# Mock PR creation result
|
|
mock_pr_result = PullRequestResult(
|
|
success=True,
|
|
pr_url="https://github.com/user/repo/pull/123",
|
|
already_exists=False,
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(
|
|
worktree_manager, "push_branch", return_value=mock_push_result
|
|
),
|
|
# Patch on the module object directly to handle importlib shim loading
|
|
patch.object(worktree_module, "detect_git_provider", return_value="github"),
|
|
patch.object(
|
|
worktree_manager, "create_pull_request", return_value=mock_pr_result
|
|
),
|
|
):
|
|
result = worktree_manager.push_and_create_pr(
|
|
spec_name=spec_name,
|
|
target_branch="main",
|
|
title="Test PR",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify provider field
|
|
assert result["provider"] == "github", (
|
|
f"Expected provider='github', got '{result['provider']}'"
|
|
)
|
|
assert result["pushed"] is True
|
|
|
|
|
|
class TestGitHubCLIInvocation:
|
|
"""Test that gh CLI is still invoked correctly with proper arguments."""
|
|
|
|
def test_gh_cli_invoked_with_correct_args(self, tmp_path):
|
|
"""Test that gh pr create is invoked with correct arguments."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info to return a valid WorktreeInfo
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Mock subprocess result
|
|
mock_subprocess_result = MagicMock(
|
|
returncode=0,
|
|
stdout="https://github.com/user/repo/pull/123\n",
|
|
stderr="",
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(
|
|
worktree_module, "get_gh_executable", return_value="/usr/bin/gh"
|
|
),
|
|
patch.object(
|
|
worktree_module.subprocess, "run", return_value=mock_subprocess_result
|
|
) as mock_run,
|
|
patch.object(manager, "_extract_spec_summary", return_value="Test PR body"),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="main",
|
|
title="Test PR Title",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify gh CLI was called with correct arguments
|
|
assert mock_run.called
|
|
call_args = mock_run.call_args[0][0]
|
|
assert call_args[0] == "/usr/bin/gh"
|
|
assert "pr" in call_args
|
|
assert "create" in call_args
|
|
assert "--base" in call_args
|
|
assert "main" in call_args
|
|
assert "--title" in call_args
|
|
assert "Test PR Title" in call_args
|
|
assert "--body" in call_args
|
|
|
|
# Verify result
|
|
assert result["success"] is True
|
|
assert result["pr_url"] == "https://github.com/user/repo/pull/123"
|
|
|
|
def test_gh_cli_draft_flag(self, tmp_path):
|
|
"""Test that --draft flag is passed to gh CLI when draft=True."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Mock subprocess result
|
|
mock_subprocess_result = MagicMock(
|
|
returncode=0,
|
|
stdout="https://github.com/user/repo/pull/123\n",
|
|
stderr="",
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(
|
|
worktree_module, "get_gh_executable", return_value="/usr/bin/gh"
|
|
),
|
|
patch.object(
|
|
worktree_module.subprocess, "run", return_value=mock_subprocess_result
|
|
) as mock_run,
|
|
patch.object(manager, "_extract_spec_summary", return_value="Test PR body"),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="main",
|
|
title="Draft PR",
|
|
draft=True,
|
|
)
|
|
|
|
# Verify --draft flag is present
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "--draft" in call_args
|
|
assert result["success"] is True
|
|
|
|
|
|
class TestGitHubOriginPrefixStripping:
|
|
"""Test that origin/ prefix is stripped from target_branch in create_pull_request."""
|
|
|
|
def test_origin_prefix_stripped_from_target_branch(self, tmp_path):
|
|
"""Test that 'origin/develop' becomes 'develop' in --base argument to gh CLI."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info to return a valid WorktreeInfo
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Mock subprocess result
|
|
mock_subprocess_result = MagicMock(
|
|
returncode=0,
|
|
stdout="https://github.com/user/repo/pull/123\n",
|
|
stderr="",
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(
|
|
worktree_module, "get_gh_executable", return_value="/usr/bin/gh"
|
|
),
|
|
patch.object(
|
|
worktree_module.subprocess, "run", return_value=mock_subprocess_result
|
|
) as mock_run,
|
|
patch.object(manager, "_extract_spec_summary", return_value="Test PR body"),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="origin/develop",
|
|
title="Test PR Title",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify gh CLI received "develop" (not "origin/develop") as --base
|
|
assert mock_run.called
|
|
call_args = mock_run.call_args[0][0]
|
|
base_idx = call_args.index("--base")
|
|
assert call_args[base_idx + 1] == "develop", (
|
|
f"Expected 'develop' after --base, got '{call_args[base_idx + 1]}'"
|
|
)
|
|
assert result["success"] is True
|
|
|
|
def test_target_branch_without_origin_prefix_unchanged(self, tmp_path):
|
|
"""Test that 'develop' (no prefix) is passed through unchanged to gh CLI."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info to return a valid WorktreeInfo
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Mock subprocess result
|
|
mock_subprocess_result = MagicMock(
|
|
returncode=0,
|
|
stdout="https://github.com/user/repo/pull/123\n",
|
|
stderr="",
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(
|
|
worktree_module, "get_gh_executable", return_value="/usr/bin/gh"
|
|
),
|
|
patch.object(
|
|
worktree_module.subprocess, "run", return_value=mock_subprocess_result
|
|
) as mock_run,
|
|
patch.object(manager, "_extract_spec_summary", return_value="Test PR body"),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="develop",
|
|
title="Test PR Title",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify gh CLI received "develop" as --base
|
|
assert mock_run.called
|
|
call_args = mock_run.call_args[0][0]
|
|
base_idx = call_args.index("--base")
|
|
assert call_args[base_idx + 1] == "develop", (
|
|
f"Expected 'develop' after --base, got '{call_args[base_idx + 1]}'"
|
|
)
|
|
assert result["success"] is True
|
|
|
|
|
|
class TestGitHubErrorHandling:
|
|
"""Test that GitHub error handling still works correctly."""
|
|
|
|
def test_missing_gh_cli_error(self, tmp_path):
|
|
"""Test error message when gh CLI is not installed."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(worktree_module, "get_gh_executable", return_value=None),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="main",
|
|
title="Test PR",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify error message
|
|
assert result["success"] is False
|
|
assert "GitHub CLI (gh) not found" in result["error"]
|
|
|
|
def test_already_exists_handling(self, tmp_path):
|
|
"""Test that 'already exists' case is handled correctly."""
|
|
# Setup
|
|
project_dir = tmp_path / "project"
|
|
project_dir.mkdir()
|
|
spec_dir = tmp_path / "spec"
|
|
spec_dir.mkdir()
|
|
|
|
# Create .auto-claude directories
|
|
auto_claude_dir = project_dir / ".auto-claude"
|
|
auto_claude_dir.mkdir(exist_ok=True)
|
|
|
|
# Create WorktreeManager
|
|
manager = WorktreeManager(
|
|
project_dir=project_dir,
|
|
base_branch="main",
|
|
)
|
|
|
|
# Mock get_worktree_info
|
|
mock_worktree_info = WorktreeInfo(
|
|
path=spec_dir,
|
|
branch="auto-claude/001-test-spec",
|
|
spec_name="001-test-spec",
|
|
base_branch="main",
|
|
is_active=True,
|
|
)
|
|
|
|
# Mock subprocess result for "already exists" error
|
|
mock_subprocess_result = MagicMock(
|
|
returncode=1,
|
|
stdout="",
|
|
stderr="pull request already exists",
|
|
)
|
|
|
|
# Import the actual module to patch it directly
|
|
import core.worktree as worktree_module
|
|
|
|
with (
|
|
patch.object(manager, "get_worktree_info", return_value=mock_worktree_info),
|
|
patch.object(
|
|
worktree_module, "get_gh_executable", return_value="/usr/bin/gh"
|
|
),
|
|
patch.object(
|
|
worktree_module.subprocess, "run", return_value=mock_subprocess_result
|
|
),
|
|
patch.object(
|
|
manager,
|
|
"_get_existing_pr_url",
|
|
return_value="https://github.com/user/repo/pull/123",
|
|
),
|
|
patch.object(manager, "_extract_spec_summary", return_value="Test PR body"),
|
|
):
|
|
result = manager.create_pull_request(
|
|
spec_name="001-test-spec",
|
|
target_branch="main",
|
|
title="Test PR",
|
|
draft=False,
|
|
)
|
|
|
|
# Verify it's treated as success with already_exists flag
|
|
assert result["success"] is True
|
|
assert result["already_exists"] is True
|
|
assert result["pr_url"] == "https://github.com/user/repo/pull/123"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|