mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(git): fetch and track missing parent branch during leaf PR merge
_sync_target_branch previously did a bare 'git checkout <target>' with no fallback. In the shared-agent-clone model the leaf developer's workspace often has only the task branch locally; the parent/cell branch exists only on origin. That produced a SERVICE_ERROR which cycled the task back to blocked every time the PM retried complete(). Now, when checkout fails, we fetch the target branch from origin and create a tracking branch before the pull. Includes regression tests for the local, fallback, and origin-missing paths.
This commit is contained in:
@@ -15,7 +15,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from roboco.api.schemas.git import GitCreateBranchRequest
|
||||
from roboco.config import settings
|
||||
from roboco.exceptions import GitCommandError
|
||||
from roboco.exceptions import GitCommandError, GitError
|
||||
from roboco.services.base import NotFoundError, UnauthorizedError
|
||||
from roboco.services.git import GitService
|
||||
|
||||
@@ -797,3 +797,91 @@ async def test_push_propagates_non_gh001_error_unchanged() -> None:
|
||||
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
|
||||
with pytest.raises(GitCommandError, match="Authentication failed"):
|
||||
await svc.push(Path("/tmp/ws"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _sync_target_branch fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_target_branch_uses_local_ref_when_present() -> None:
|
||||
"""If the target branch already exists locally, just checkout + pull."""
|
||||
svc = _service()
|
||||
calls: list[list[str]] = []
|
||||
|
||||
async def _run_git(_workspace: object, args: list[str], **_kw: object) -> object:
|
||||
calls.append(args)
|
||||
if args[:2] == ["checkout", "feature/backend/parent"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:1] == ["pull"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:2] == ["log", "-1"]:
|
||||
return MagicMock(returncode=0, stdout="local-tip-sha", stderr="")
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
|
||||
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
|
||||
sha = await svc._sync_target_branch(
|
||||
Path("/tmp/ws"), "feature/backend/parent", "token"
|
||||
)
|
||||
assert sha == "local-tip-sha"
|
||||
assert ["checkout", "feature/backend/parent"] in calls
|
||||
assert ["pull"] in calls
|
||||
assert ["fetch", "origin", "feature/backend/parent"] not in calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_target_branch_fetches_and_tracks_when_local_ref_missing() -> None:
|
||||
"""A parent branch that only exists on origin is fetched and tracked."""
|
||||
svc = _service()
|
||||
calls: list[list[str]] = []
|
||||
|
||||
async def _run_git(_workspace: object, args: list[str], **_kw: object) -> object:
|
||||
calls.append(args)
|
||||
if args[:2] == ["checkout", "feature/backend/parent"]:
|
||||
return MagicMock(returncode=1, stdout="", stderr="pathspec did not match")
|
||||
if args[:2] == ["fetch", "origin"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:3] == ["checkout", "-b", "feature/backend/parent"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:1] == ["pull"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:2] == ["log", "-1"]:
|
||||
return MagicMock(returncode=0, stdout="origin-tip-sha", stderr="")
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
|
||||
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
|
||||
sha = await svc._sync_target_branch(
|
||||
Path("/tmp/ws"), "feature/backend/parent", "token"
|
||||
)
|
||||
assert sha == "origin-tip-sha"
|
||||
assert ["checkout", "feature/backend/parent"] in calls
|
||||
assert ["fetch", "origin", "feature/backend/parent"] in calls
|
||||
assert [
|
||||
"checkout",
|
||||
"-b",
|
||||
"feature/backend/parent",
|
||||
"origin/feature/backend/parent",
|
||||
] in calls
|
||||
assert ["pull"] in calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_target_branch_raises_when_origin_also_missing() -> None:
|
||||
"""If the branch is missing locally AND on origin, raise a clear GitError."""
|
||||
svc = _service()
|
||||
|
||||
async def _run_git(_workspace: object, args: list[str], **_kw: object) -> object:
|
||||
if args[:2] == ["checkout", "feature/backend/parent"]:
|
||||
return MagicMock(returncode=1, stdout="", stderr="local missing")
|
||||
if args[:2] == ["fetch", "origin"]:
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
if args[:3] == ["checkout", "-b", "feature/backend/parent"]:
|
||||
return MagicMock(returncode=1, stdout="", stderr="origin missing")
|
||||
return MagicMock(returncode=0, stdout="", stderr="")
|
||||
|
||||
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
|
||||
with pytest.raises(GitError, match="Could not check out target branch"):
|
||||
await svc._sync_target_branch(
|
||||
Path("/tmp/ws"), "feature/backend/parent", "token"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user