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:
@@ -899,10 +899,22 @@ async def test_pr_review_gate_pass_path(
|
||||
assert reviewer_row.status == AgentStatus.ACTIVE
|
||||
assert reviewer_row.current_task_id == task.id
|
||||
|
||||
# Resolved via the same team-based query pr_pass itself uses (rather than
|
||||
# assumed to be this fixture's own cell_pm_agent) — the shared/cumulative
|
||||
# integration DB may carry other BACKEND/CELL_PM agents from earlier
|
||||
# tests, and _agent_with_role_and_team has no ordering guarantee.
|
||||
expected_pm = await svc.cell_pm_for_team(Team.BACKEND)
|
||||
assert expected_pm is not None
|
||||
|
||||
passed = await svc.pr_pass(reviewer_id, task.id, notes="integration verified")
|
||||
assert passed is not None
|
||||
assert str(passed.status) == Status.AWAITING_PM_REVIEW.value
|
||||
assert passed.assigned_to is None # cleared so the PM-closure dispatch routes
|
||||
# Hands off to the owning cell PM (team-resolved) rather than clearing —
|
||||
# AWAITING_PM_REVIEW has no claim() edge, so an unassigned task here has
|
||||
# no way back to a PM.
|
||||
assert passed.assigned_to == expected_pm.id
|
||||
assert passed.claimed_by == expected_pm.id
|
||||
assert passed.active_claimant_id is None
|
||||
# pr_pass releases the reviewer's fleet marker too.
|
||||
reviewer_row = await db_session.get(AgentTable, reviewer_id)
|
||||
assert reviewer_row is not None
|
||||
|
||||
@@ -797,6 +797,53 @@ async def test_unblock_unknown_returns_404(task_client: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_review_pm_unknown_returns_404(task_client: dict) -> None:
|
||||
client = task_client["client"]
|
||||
response = await client.post(f"/api/tasks/{uuid4()}/assign-review-pm", headers=_HDR)
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_review_pm_places_owning_pm(task_client: dict) -> None:
|
||||
"""The orchestrator's recovery seam: a review task with no (or a stale)
|
||||
owner is placed with the real team-resolved PM — CLAIM_RULES has no
|
||||
claim() edge into AWAITING_PM_REVIEW for the normal route to do this.
|
||||
|
||||
Resolves the expected PM via the real ``main_pm_agent()`` query rather
|
||||
than assuming this fixture's own agent — the test DB is shared/cumulative
|
||||
across the module, and "earliest-created" main_pm may be an older row
|
||||
from an earlier test.
|
||||
"""
|
||||
setup = task_client
|
||||
client = setup["client"]
|
||||
task = _seed_task(
|
||||
setup,
|
||||
status=TaskStatus.AWAITING_PM_REVIEW,
|
||||
team=Team.MAIN_PM,
|
||||
assigned_to=None,
|
||||
)
|
||||
await setup["db"].commit()
|
||||
expected_pm = await TaskService(setup["db"]).main_pm_agent()
|
||||
assert expected_pm is not None
|
||||
|
||||
response = await client.post(f"/api/tasks/{task.id}/assign-review-pm", headers=_HDR)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
body = response.json()
|
||||
assert body["assigned_to"] == str(expected_pm.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_review_pm_rejects_non_review_status(task_client: dict) -> None:
|
||||
setup = task_client
|
||||
client = setup["client"]
|
||||
task = _seed_task(setup, status=TaskStatus.IN_PROGRESS)
|
||||
await setup["db"].commit()
|
||||
|
||||
response = await client.post(f"/api/tasks/{task.id}/assign-review-pm", headers=_HDR)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pause_unknown_returns_404(task_client: dict) -> None:
|
||||
client = task_client["client"]
|
||||
|
||||
Reference in New Issue
Block a user