diff --git a/roboco/services/git.py b/roboco/services/git.py index b583cde5..f40ad7c4 100644 --- a/roboco/services/git.py +++ b/roboco/services/git.py @@ -3363,7 +3363,16 @@ class GitService(BaseService): workspace = await self._workspace_for_branch( branch_name, actor_agent_id=actor_agent_id ) - return await self.push(workspace) + # Push the NAMED branch, not the workspace's current checkout. The + # clone root is shared across a dev's tasks and (F123) parked on the + # default branch while the task branch lives in a per-task worktree; + # push() with branch=None defaults to get_current_branch(workspace), + # which pushed the wrong ref. The dev's commit then never reached + # origin, so create_pr 422'd with "No commits between" and the dev was + # forced into i_am_blocked — stranded work the PM's unblock can't fix + # (it only flips status, not git state). Push-by-name also survives a + # clone parked on a LATER task's branch. + return await self.push(workspace, branch=branch_name) async def _ensure_base_on_remote( self, diff --git a/tests/unit/services/test_git.py b/tests/unit/services/test_git.py index 5e8fda9d..34e0b11a 100644 --- a/tests/unit/services/test_git.py +++ b/tests/unit/services/test_git.py @@ -160,6 +160,35 @@ async def test_push_task_branch_pushes_task_branch_by_name() -> None: assert_branch.assert_not_awaited() +@pytest.mark.asyncio +async def test_push_branch_pushes_named_branch_not_current_checkout() -> None: + """push_branch (open_pr's push side effect) pushes the NAMED branch. + + The clone root is shared across a dev's tasks and (F123) parked on the + default branch while the task branch lives in a per-task worktree. + push_branch used to call push(workspace) with no ``branch`` arg, so + push() fell back to get_current_branch(workspace) and pushed the wrong + ref (the clone root's checkout, e.g. the default branch). The dev's + commit then never reached origin, create_pr 422'd with "No commits + between", and the dev was forced into i_am_blocked — stranded work the + PM's unblock cannot repair (it flips status, not git state). The named + branch must be passed through to push(). + """ + branch_name = "feature/backend/fb836f80--03f80432--d3dab0fc--b04afcb5" + svc = _service() + _bind(svc, "_workspace_for_branch", AsyncMock(return_value=Path("/tmp/ws"))) + push_mock = AsyncMock(return_value=(branch_name, _PUSHED_COMMIT_COUNT)) + _bind(svc, "push", push_mock) + + result_branch, pushed = await svc.push_branch(branch_name) + + assert result_branch == branch_name + assert pushed == _PUSHED_COMMIT_COUNT + # The named branch is forwarded to push() — NOT push(workspace) which + # would default to the clone root's current checkout. + push_mock.assert_awaited_once_with(Path("/tmp/ws"), branch=branch_name) + + @pytest.mark.asyncio async def test_push_targets_explicit_branch_not_current_checkout() -> None: """push(branch=X) pushes X by ref even when the workspace is on Y."""