mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(git): create_pr auto-creates a missing PR base branch on origin
open_pr -> GitService.create_pr posted "base": parent straight to GitHub, so
when the parent (an ancestor task's integration branch) was never pushed — a PM
paused before its first push, or the workspace was wiped — GitHub 422'd "base
field invalid" and stranded every child PR. The base-existence fallback added
in 3d9dd298 lived only in create_pull_request, which open_pr never calls.
Add _ensure_base_on_remote and call it in create_pr: if the base branch is
absent on origin, create it off the default branch's tip (preserving the
integration hierarchy) instead of failing; fall back to the default branch only
if that create push itself fails. Covered by 3 new tests.
This commit is contained in:
@@ -294,6 +294,8 @@ async def test_create_pr_returns_pr_dict() -> None:
|
||||
_bind(svc, "_get_project_token_or_raise", AsyncMock(return_value="tok"))
|
||||
_bind(svc, "_parse_github_remote", MagicMock(return_value=("acme", "repo")))
|
||||
_bind(svc, "_record_pr_atomically", AsyncMock())
|
||||
# parent == default → _ensure_base_on_remote short-circuits (no git call)
|
||||
_bind(svc, "_project_default_branch", AsyncMock(return_value="master"))
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.is_success = True
|
||||
@@ -321,6 +323,80 @@ async def test_create_pr_raises_when_branch_not_found() -> None:
|
||||
await svc.create_pr("missing/branch", parent="master", is_root_pr=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _ensure_base_on_remote: create the PR base branch if it's missing on origin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_base_creates_missing_base_off_default() -> None:
|
||||
"""Missing base branch is created on origin off the default branch tip."""
|
||||
svc = _service()
|
||||
_bind(svc, "_project_default_branch", AsyncMock(return_value="master"))
|
||||
calls: list[list[str]] = []
|
||||
|
||||
async def fake_run_git(_ws: Path, args: list[str], **_: object) -> MagicMock:
|
||||
calls.append(args)
|
||||
if args[0] == "ls-remote":
|
||||
return MagicMock(stdout="", returncode=0, stderr="") # base absent
|
||||
return MagicMock(stdout="", returncode=0, stderr="")
|
||||
|
||||
_bind(svc, "_run_git", fake_run_git)
|
||||
out = await svc._ensure_base_on_remote(
|
||||
Path("/tmp/ws"), "feature/frontend/abc--def", "roboco", "tok"
|
||||
)
|
||||
assert out == "feature/frontend/abc--def"
|
||||
assert any(
|
||||
a[0] == "push" and a[-1] == "origin/master:refs/heads/feature/frontend/abc--def"
|
||||
for a in calls
|
||||
), calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_base_passthrough_when_present() -> None:
|
||||
"""An existing base branch is returned unchanged with no push."""
|
||||
svc = _service()
|
||||
_bind(svc, "_project_default_branch", AsyncMock(return_value="master"))
|
||||
pushed = False
|
||||
|
||||
async def fake_run_git(_ws: Path, args: list[str], **_: object) -> MagicMock:
|
||||
nonlocal pushed
|
||||
if args[0] == "push":
|
||||
pushed = True
|
||||
if args[0] == "ls-remote":
|
||||
return MagicMock(
|
||||
stdout="sha\trefs/heads/feature/x", returncode=0, stderr=""
|
||||
)
|
||||
return MagicMock(stdout="", returncode=0, stderr="")
|
||||
|
||||
_bind(svc, "_run_git", fake_run_git)
|
||||
out = await svc._ensure_base_on_remote(
|
||||
Path("/tmp/ws"), "feature/x", "roboco", "tok"
|
||||
)
|
||||
assert out == "feature/x"
|
||||
assert pushed is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_base_falls_back_to_default_when_create_fails() -> None:
|
||||
"""If the create push fails, retarget to the default branch (never 422)."""
|
||||
svc = _service()
|
||||
_bind(svc, "_project_default_branch", AsyncMock(return_value="master"))
|
||||
|
||||
async def fake_run_git(_ws: Path, args: list[str], **_: object) -> MagicMock:
|
||||
if args[0] == "ls-remote":
|
||||
return MagicMock(stdout="", returncode=0, stderr="")
|
||||
if args[0] == "push":
|
||||
return MagicMock(stdout="", returncode=1, stderr="denied")
|
||||
return MagicMock(stdout="", returncode=0, stderr="")
|
||||
|
||||
_bind(svc, "_run_git", fake_run_git)
|
||||
out = await svc._ensure_base_on_remote(
|
||||
Path("/tmp/ws"), "feature/x", "roboco", "tok"
|
||||
)
|
||||
assert out == "master"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# pr_merge: returns merge commit dict
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user