mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
test(gateway): regression for ContentActions.commit role gate
Pin: PM, QA, Board cannot call commit; developer + documenter can. Smoke 2026-05-03 saw main-pm reach the git layer with a 'commit' call attempting to author a fix to the very gateway bug we were hitting.
This commit is contained in:
@@ -17,14 +17,12 @@ def _make_deps(**overrides: AsyncMock) -> ContentActionsDeps:
|
|||||||
else:
|
else:
|
||||||
task = AsyncMock()
|
task = AsyncMock()
|
||||||
task.get_active_task_for_agent.return_value = None
|
task.get_active_task_for_agent.return_value = None
|
||||||
|
# commit() checks caller role server-side; default-created mocks
|
||||||
|
# need a default developer role so existing tests pass through.
|
||||||
|
# Caller-supplied mocks must set agent_for themselves.
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
# commit() now checks the caller's role server-side. Default the
|
task.agent_for.return_value = MagicMock(role="developer")
|
||||||
# mock's agent_for to a developer so existing commit tests still
|
|
||||||
# exercise the success path; tests asserting the role gate should
|
|
||||||
# override task.agent_for AFTER _make_deps returns.
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
task.agent_for.return_value = MagicMock(role="developer")
|
|
||||||
|
|
||||||
if "git" in overrides:
|
if "git" in overrides:
|
||||||
git = overrides["git"]
|
git = overrides["git"]
|
||||||
@@ -78,6 +76,7 @@ async def test_commit_descriptive_with_active_task_succeeds() -> None:
|
|||||||
)
|
)
|
||||||
task_svc = AsyncMock()
|
task_svc = AsyncMock()
|
||||||
task_svc.get_active_task_for_agent.return_value = task_obj
|
task_svc.get_active_task_for_agent.return_value = task_obj
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="developer")
|
||||||
git_svc = AsyncMock()
|
git_svc = AsyncMock()
|
||||||
git_svc.commit.return_value = {"sha": "deadbeef1234"}
|
git_svc.commit.return_value = {"sha": "deadbeef1234"}
|
||||||
|
|
||||||
@@ -129,6 +128,7 @@ async def test_commit_strips_existing_task_prefix() -> None:
|
|||||||
)
|
)
|
||||||
task_svc = AsyncMock()
|
task_svc = AsyncMock()
|
||||||
task_svc.get_active_task_for_agent.return_value = task_obj
|
task_svc.get_active_task_for_agent.return_value = task_obj
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="developer")
|
||||||
git_svc = AsyncMock()
|
git_svc = AsyncMock()
|
||||||
git_svc.commit.return_value = {"sha": "cafebabe"}
|
git_svc.commit.return_value = {"sha": "cafebabe"}
|
||||||
|
|
||||||
@@ -154,6 +154,72 @@ async def test_commit_strips_existing_task_prefix() -> None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_rejects_pm_role() -> None:
|
||||||
|
"""Regression: PMs must not be able to call commit (smoke 2026-05-03)."""
|
||||||
|
agent_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="main_pm")
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.commit(agent_id=agent_id, message="fix: something")
|
||||||
|
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["error"] == "not_authorized"
|
||||||
|
assert "main_pm" in body["message"]
|
||||||
|
assert "delegate" in body["remediate"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_rejects_qa_role() -> None:
|
||||||
|
"""QA cannot author commits — only developers and documenters can."""
|
||||||
|
agent_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="qa")
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.commit(agent_id=agent_id, message="fix: bug")
|
||||||
|
|
||||||
|
assert env.as_dict()["error"] == "not_authorized"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_rejects_board_role() -> None:
|
||||||
|
"""Board members do not write code."""
|
||||||
|
agent_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="product_owner")
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.commit(agent_id=agent_id, message="fix: thing")
|
||||||
|
|
||||||
|
assert env.as_dict()["error"] == "not_authorized"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_allows_documenter_role() -> None:
|
||||||
|
"""Documenters write doc commits — must not be rejected."""
|
||||||
|
agent_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
task_obj = MagicMock(
|
||||||
|
id=task_id, status="awaiting_documentation", branch_name="feature/backend/abc"
|
||||||
|
)
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="documenter")
|
||||||
|
task_svc.get_active_task_for_agent.return_value = task_obj
|
||||||
|
git_svc = AsyncMock()
|
||||||
|
git_svc.commit.return_value = {"sha": "deadbeef"}
|
||||||
|
deps = _make_deps(task=task_svc, git=git_svc)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.commit(agent_id=agent_id, message="docs: add migration notes")
|
||||||
|
|
||||||
|
assert env.error is None # not rejected on role grounds
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_note_reflect_scope_succeeds() -> None:
|
async def test_note_reflect_scope_succeeds() -> None:
|
||||||
"""scope='reflect' is valid; journal.write_entry is called."""
|
"""scope='reflect' is valid; journal.write_entry is called."""
|
||||||
|
|||||||
Reference in New Issue
Block a user