Files
roboco/tests/unit/foundation/policy/test_sync_branch_intent.py
T

78 lines
2.8 KiB
Python
Raw Normal View History

2026-06-29 05:38:21 +02:00
"""IntentSpec for the dev `sync_branch` verb (multi-level sequencing Phase B1).
Raw shell git is denied to agents by design (the `Bash(git:*)` base deny), so a
developer whose branch has fallen behind its base had no gate-level way to
rebase — only the CEO/PM-only `/rebase` HTTP route. `sync_branch` is the dev
verb that wraps the git rebase through the gate (traced + evidenced), so the
"everything goes through the gates" invariant holds. These tests lock the spec
declaration: dev-only, ownership-gated, composes nothing (git-only, no DB
transition), and present in the dev flow.
"""
from __future__ import annotations
from dataclasses import dataclass
from uuid import uuid4
from roboco.foundation.identity import Role
from roboco.foundation.policy.lifecycle import (
Context,
Decision,
can_invoke_intent,
intents_for_role,
)
from roboco.services.gateway.role_config import _DEV_FLOW
def test_sync_branch_is_a_dev_flow_verb() -> None:
# Declared with _DEV_ROLES, so intents_for_role propagates it into the dev
# flow automatically — no role_config edit needed (the spec is canon).
assert "sync_branch" in intents_for_role(Role.DEVELOPER)
assert "sync_branch" in _DEV_FLOW
def test_sync_branch_is_dev_only() -> None:
# QA / documenter / PM cannot call it — it's a developer's branch-sync verb.
for role in (Role.QA, Role.DOCUMENTER, Role.CELL_PM, Role.MAIN_PM):
assert "sync_branch" not in intents_for_role(role), (
f"{role} must not get sync_branch"
)
@dataclass
class _Task:
assigned_to: object = None
def test_sync_branch_requires_ownership() -> None:
# PRECONDITION_OWNERSHIP (rejection_kind='not_authorized') gates it: a task
# assigned to another agent rejects as not_authorized, not a tracing gap.
owner = uuid4()
other = uuid4()
task = _Task(assigned_to=other)
decision = can_invoke_intent(
Role.DEVELOPER, "sync_branch", task, Context(actor_id=owner)
)
assert not decision.allowed
assert decision.rejection_kind == "not_authorized"
def test_sync_branch_allowed_when_owner() -> None:
# composes=() and the only precondition is ownership, so the owner passes the
# spec gate. (The choreographer handler does the git work + branch/base
# guards separately.)
owner = uuid4()
task = _Task(assigned_to=owner)
decision = can_invoke_intent(
Role.DEVELOPER, "sync_branch", task, Context(actor_id=owner)
)
assert isinstance(decision, Decision)
assert decision.allowed
def test_sync_branch_unknown_to_other_role_rejects() -> None:
# A role not in allowed_roles is rejected as not_authorized (role gating).
decision = can_invoke_intent(Role.QA, "sync_branch", _Task(), Context())
assert not decision.allowed
assert decision.rejection_kind == "not_authorized"