mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(lifecycle): pr_pass hands ownership to the owning PM — closes the passed-PR completion wedge
Removing the awaiting_pm_review re-claim edge (d87e2d9b, the #740
review-loop fix) exposed that the edge was load-bearing: pr_pass
cleared assigned_to/claimed_by to None and the re-claim was the only
way a PM ever re-acquired the task. Since then every assembled task
passing the PR gate wedged: the closure PM's complete rejected with
'not assigned to you', its fallback claim rejected (edge gone), and
its only exit was escalate_up — BLOCKING the task onto main-pm, who
is not the assignee either and burned spawns doing nothing. Live:
PR #741's task (7 ownership rejections, escalated 13:59Z), plus two
more tasks with 25 and 2 rejections in the same shape.
pr_pass now resolves the owning PM via _revision_pm_for_task and
assigns it, exactly as pr_fail always did — one chokepoint covering
cell (submit_up) and root (submit_root) tasks. mark_pr_created's
ready_for_pm branch (leaf docs-path, PR-arrives-second) had the same
clear-to-None wedge and now resolves via _resolve_pm_for_review like
its docs-first sibling. No claim edge is reintroduced; the #740
parity test is untouched.
Recovery for already-wedged tasks needs no DB surgery: new idempotent
TaskService.assign_review_pm + POST /tasks/{id}/assign-review-pm
(ASSIGN-gated, explicit commit) corrects an unassigned OR mis-assigned
awaiting_pm_review task to its owning PM; _dispatch_pm_review_work
ensures assignment before every spawn (cheap pre-check skips the
round-trip when the fetched assigned_to already matches), replacing
the dead _claim_task_for_agent call whose lifecycle claim the removed
edge now always rejects; _maybe_spawn_pm_closure routes through
_closure_review_pm, which keeps the team-resolved PM whenever the
assign route fails so a transient error can never spawn a stale
assignee.
Gate: 15506 passed, 459 skipped; xenon/ruff/mypy/vulture/bandit/
pip-audit/deptry/import-linter/foundation-check green.
This commit is contained in:
@@ -495,6 +495,47 @@ async def test_cell_pm_complete_not_assigned_returns_not_authorized() -> None:
|
||||
assert env.as_dict()["error"] == "not_authorized"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cell_pm_complete_passes_ownership_guard_after_pr_pass_handoff() -> None:
|
||||
"""#740 (d87e2d9b) removed the illegal awaiting_pm_review -> claimed
|
||||
re-claim edge, which used to be the PM's only way back to ownership
|
||||
after the PR gate — pr_pass cleared assigned_to to None, so the owning
|
||||
PM's complete() dead-ended on this exact guard forever
|
||||
(not_authorized). pr_pass now hands off to the resolved owning PM
|
||||
instead (see TaskService.pr_pass), so a task in the real post-gate
|
||||
shape — assigned_to == the calling PM, no subtasks — must clear this
|
||||
guard rather than bounce."""
|
||||
pm_id = uuid4()
|
||||
task_id = uuid4()
|
||||
t = MagicMock(
|
||||
id=task_id,
|
||||
status="awaiting_pm_review",
|
||||
assigned_to=pm_id,
|
||||
pr_number=8,
|
||||
branch_name="feature/backend/abc--def",
|
||||
parent_task_id=None,
|
||||
team="backend",
|
||||
)
|
||||
after = MagicMock(**{**t.__dict__, "status": "completed"})
|
||||
task_svc = AsyncMock()
|
||||
task_svc.get.return_value = t
|
||||
task_svc.all_subtasks_terminal.return_value = True
|
||||
task_svc.cell_pm_complete.return_value = after
|
||||
git_svc = AsyncMock()
|
||||
git_svc.is_pr_merged_for_task.return_value = False
|
||||
git_svc.pr_merge.return_value = {"merged": True, "merge_commit_sha": "merge-abc"}
|
||||
journal_svc = AsyncMock()
|
||||
journal_svc.has_decision_for_task.return_value = True
|
||||
journal_svc.latest_decision_at.return_value = datetime.now(UTC)
|
||||
journal_svc.has_reflect_for_task.return_value = True
|
||||
deps = _make_deps(task=task_svc, git=git_svc, journal=journal_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.cell_pm_complete(pm_id, task_id, notes="reviewed and approved")
|
||||
assert env.as_dict().get("error") != "not_authorized"
|
||||
assert env.error is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cell_pm_complete_in_progress_steers_to_submit_up() -> None:
|
||||
"""Mirror of the main-PM submit_root steer: a cell task still in_progress
|
||||
|
||||
Reference in New Issue
Block a user