fix(gateway): root PR base resolves the project's env ladder, not literal master

A parentless root's PR base / merge target now resolves through
resolve_parent_branch to the project's panel-configured head rung —
submit_root passed a hardcoded 'master', which on a main-default repo made
_ensure_base_on_remote silently create a spurious master branch and land
the assembled root PR there. Literal master survives only as the
no-project string-derivation fallback.
This commit is contained in:
Renn F
2026-07-18 16:41:17 +02:00
parent 99224e65cd
commit 6a30cca4af
4 changed files with 38 additions and 9 deletions
+15 -2
View File
@@ -72,10 +72,23 @@ class TestResolveParentBranch:
task_service.get.assert_awaited_once_with(root_id)
@pytest.mark.asyncio
async def test_falls_back_when_no_parent(self) -> None:
async def test_parentless_root_uses_project_head_rung(self) -> None:
# A parentless root's branch was cut from the project's head rung
# (panel-configured env ladder), so that rung is the PR base / merge
# target — never a literal "master", which on a main-default repo
# silently targets a branch the project doesn't use.
task = MagicMock(parent_task_id=None, branch_name="feature/backend/ROOT0001")
task_service = AsyncMock()
# No parent → root→master.
task_service.project_default_branch_for_task = AsyncMock(return_value="main")
assert await resolve_parent_branch(task, task_service) == "main"
task_service.get.assert_not_called()
@pytest.mark.asyncio
async def test_parentless_root_falls_back_to_string_when_no_project(self) -> None:
task = MagicMock(parent_task_id=None, branch_name="feature/backend/ROOT0001")
task_service = AsyncMock()
task_service.project_default_branch_for_task = AsyncMock(return_value=None)
# No project to consult → string derivation's master fallback.
assert await resolve_parent_branch(task, task_service) == "master"
task_service.get.assert_not_called()
+4
View File
@@ -363,6 +363,7 @@ async def test_runner_forwards_actor_agent_id_to_create_root_pr() -> None:
task_svc.submit_for_review = AsyncMock(
return_value=MagicMock(status="awaiting_pr_review")
)
task_svc.project_default_branch_for_task = AsyncMock(return_value="main")
git_svc = AsyncMock()
git_svc.create_pr = AsyncMock(return_value={"pr_number": 7})
runner = VerbRunner(task_service=task_svc, git_service=git_svc)
@@ -381,6 +382,9 @@ async def test_runner_forwards_actor_agent_id_to_create_root_pr() -> None:
assert git_svc.create_pr.call_args.kwargs.get("is_root_pr") is True
assert git_svc.create_pr.call_args.kwargs.get("actor_agent_id") == agent.id
assert git_svc.create_pr.call_args.kwargs.get("parent") == "main", (
"root PR base must be the project's head rung, not a literal master"
)
@pytest.mark.asyncio