mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[feature] sync_branch dev verb — gate-level branch rebase (Phase B1)
Raw shell git is denied to agents (Bash(git:*) base deny), so a developer whose branch fell behind its base had no gate-level rebase — only the CEO/PM-only /rebase HTTP route. sync_branch is the dev verb that wraps the rebase through the gate (traced + evidenced), so the 'everything goes through the gates' invariant holds. - lifecycle: IntentSpec sync_branch (dev-only, ownership-gated, composes=(), git-only — no DB transition); _next_hint_synced helper. - GitService.sync_task_branch: rebase task.branch_name onto its resolved base via rebase_onto_base (fetch + rebase + force-with-lease push). - Choreographer.sync_branch + _sync_branch_preflight_rejection: not_found / unknown-role / spec-gate / no-branch / protected-base guards, then the git op; conflicts abort (no force-push) and steer to resolve-by-hand; git failure steers to i_am_blocked. - HTTP route /api/v1/flow/developer/sync_branch + SyncBranchRequest schema. - MCP tool sync_branch(task_id) + _TOOLS registration (manifest auto-propagates via intents_for_role(Role.DEVELOPER)). Tests: intent spec (5), choreographer handler (8: happy/conflicts/not_found/ not_authorized/no-branch/protected-base/git-failure/audit), route (1), MCP (1). ruff + mypy roboco/ tests/ clean; unit suite green (DB-fixture errors env-only).
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user