2026-05-02 03:11:49 +02:00
|
|
|
"""Tests for PR merge target resolution by task scope."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-23 05:16:56 +02:00
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
2026-05-02 03:11:49 +02:00
|
|
|
import pytest
|
2026-05-23 05:16:56 +02:00
|
|
|
from roboco.services.gateway.merge_chain import (
|
|
|
|
|
branch_depth,
|
|
|
|
|
parent_branch_for,
|
|
|
|
|
resolve_parent_branch,
|
|
|
|
|
)
|
2026-05-02 03:11:49 +02:00
|
|
|
|
|
|
|
|
_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")
|
2026-05-06 21:02:31 +02:00
|
|
|
|
|
|
|
|
|
2026-05-23 05:16:56 +02:00
|
|
|
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_falls_back_when_no_parent(self) -> None:
|
|
|
|
|
task = MagicMock(parent_task_id=None, branch_name="feature/backend/ROOT0001")
|
|
|
|
|
task_service = AsyncMock()
|
|
|
|
|
# No parent → root→master.
|
|
|
|
|
assert await resolve_parent_branch(task, task_service) == "master"
|
|
|
|
|
task_service.get.assert_not_called()
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_falls_back_when_parent_has_no_branch(self) -> None:
|
|
|
|
|
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))
|
|
|
|
|
# Parent exists but has no branch yet → string derivation.
|
|
|
|
|
result = await resolve_parent_branch(task, task_service)
|
|
|
|
|
assert result == "feature/backend/ROOT0001"
|
|
|
|
|
|
|
|
|
|
|
2026-05-06 21:02:31 +02:00
|
|
|
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")
|