Files
roboco/tests/unit/gateway/test_merge_chain.py
T
Renn F 6a30cca4af 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.
2026-07-18 16:41:17 +02:00

137 lines
5.5 KiB
Python

"""Tests for PR merge target resolution by task scope."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.services.gateway.merge_chain import (
branch_depth,
parent_branch_for,
resolve_parent_branch,
)
_DEPTH_ROOT = 1
_DEPTH_ONE_SUBTASK = 2
_DEPTH_DEEP_SUBTASK = 3
class TestBranchDepth:
def test_root_branch(self) -> None:
assert branch_depth("feature/backend/abc12345") == _DEPTH_ROOT
def test_one_subtask(self) -> None:
assert branch_depth("feature/backend/abc12345--def67890") == _DEPTH_ONE_SUBTASK
def test_deep_subtask(self) -> None:
deep = "feature/backend/abc12345--def67890--ghi11111"
assert branch_depth(deep) == _DEPTH_DEEP_SUBTASK
class TestParentBranchFor:
def test_leaf_returns_immediate_parent(self) -> None:
b = "feature/backend/abc12345--def67890--ghi11111"
assert parent_branch_for(b) == "feature/backend/abc12345--def67890"
def test_one_level_returns_root_task_branch(self) -> None:
b = "feature/backend/abc12345--def67890"
assert parent_branch_for(b) == "feature/backend/abc12345"
def test_root_returns_master(self) -> None:
b = "feature/backend/abc12345"
assert parent_branch_for(b) == "master"
def test_master_returns_master(self) -> None:
# Edge case: should be a no-op
assert parent_branch_for("master") == "master"
def test_invalid_pattern_raises(self) -> None:
with pytest.raises(ValueError, match="invalid branch"):
parent_branch_for("not-a-branch")
class TestResolveParentBranch:
"""#181/#182: base/target comes from the parent TASK's branch_name, which
is correct across a team boundary; parent_branch_for is the fallback."""
@pytest.mark.asyncio
async def test_uses_parent_task_branch_across_team(self) -> None:
root_id = uuid4()
task = MagicMock(
parent_task_id=root_id,
branch_name="feature/backend/ROOT0001--CELL0001",
)
task_service = AsyncMock()
# Root lives under a DIFFERENT team prefix than the cell.
task_service.get = AsyncMock(
return_value=MagicMock(branch_name="feature/main_pm/ROOT0001")
)
result = await resolve_parent_branch(task, task_service)
assert result == "feature/main_pm/ROOT0001"
task_service.get.assert_awaited_once_with(root_id)
@pytest.mark.asyncio
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()
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()
@pytest.mark.asyncio
async def test_branchless_parent_uses_project_default_branch(self) -> None:
# #17: a branchless coordination parent never gets a branch. The child
# was cut from its own project's default branch, so that is the real
# merge target — NOT a string-derived ref the parent never created
# (which would have no valid merge target and wedge the cell↔Main-PM
# loop).
task = MagicMock(
parent_task_id=uuid4(),
branch_name="feature/backend/ROOT0001--CELL0001",
)
task_service = AsyncMock()
task_service.get = AsyncMock(return_value=MagicMock(branch_name=None))
task_service.project_default_branch_for_task = AsyncMock(return_value="master")
result = await resolve_parent_branch(task, task_service)
assert result == "master"
task_service.project_default_branch_for_task.assert_awaited_once_with(task)
@pytest.mark.asyncio
async def test_branchless_parent_falls_back_to_string_when_no_project(self) -> None:
# No project to consult (resolver returns None) → string derivation
# remains the last-resort fallback.
task = MagicMock(
parent_task_id=uuid4(),
branch_name="feature/backend/ROOT0001--CELL0001",
)
task_service = AsyncMock()
task_service.get = AsyncMock(return_value=MagicMock(branch_name=None))
task_service.project_default_branch_for_task = AsyncMock(return_value=None)
result = await resolve_parent_branch(task, task_service)
assert result == "feature/backend/ROOT0001"
def test_branch_depth_master_is_zero() -> None:
"""Line 28: master returns 0."""
assert branch_depth("master") == 0
def test_branch_depth_invalid_pattern_raises() -> None:
"""Line 31: invalid branch pattern raises."""
with pytest.raises(ValueError, match="invalid branch"):
branch_depth("garbage-branch-name")