mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[F008] evidence_builder: surface persisted pr_review verdict+issues in the PM task_handoff
The pr_fail a2a steer to the owning PM is fire-and-forget; a PM respawned into needs_revision later read none of it (build_task_handoff never looked at notes_structured), saw a generic 'needs revision' with zero concrete change-requests, and re-submitted the same PR (the 2026-06-27 infinite pr_fail loop on 9980d0a0 / PR #138). The signal-gap was only partially closed by the a2a. build_task_handoff now extracts notes_structured.pr_review (verdict/summary/issues/head_sha — the slot pr_fail authors on every fail) into a pr_review field on the handoff, so every PM briefing for the task carries the concrete change-requests. A prior pr_fail alone now counts as prior-work-worth-resuming. Type-guarded + capped; absent => no key (no misleading empty slot). TDD red->green; ruff + mypy clean; evidence_builder suite green (14 passed).
This commit is contained in:
@@ -187,3 +187,52 @@ class TestTaskHandoff:
|
||||
assert digest["journal_highlights"] == []
|
||||
assert digest["pr_url"] is None
|
||||
assert digest["branch_name"] is None
|
||||
|
||||
|
||||
class TestPrReviewSurface:
|
||||
"""F008 — the persisted pr_fail verdict + issues must surface in the PM
|
||||
briefing's task_handoff, not just the fire-and-forget a2a. A PM respawned
|
||||
into ``needs_revision`` after a pr_fail otherwise sees a generic "needs
|
||||
revision" with zero concrete change-requests and re-submits the same PR."""
|
||||
|
||||
def test_surfaces_pr_fail_verdict_and_issues(self) -> None:
|
||||
t = _task(pr_number=138, commits=[{"sha": "abc", "message": "feat: x"}])
|
||||
t.notes_structured = {
|
||||
"pr_review": {
|
||||
"verdict": "failed",
|
||||
"summary": "In-path PR-review gate requested changes.",
|
||||
"issues": ["missing null guard", "no test for the edge case"],
|
||||
"head_sha": "aaaa1111bbbb2222",
|
||||
}
|
||||
}
|
||||
digest = build_task_handoff(t, [])
|
||||
assert digest is not None
|
||||
pr_review = digest["pr_review"]
|
||||
assert pr_review["verdict"] == "failed"
|
||||
assert pr_review["issues"] == [
|
||||
"missing null guard",
|
||||
"no test for the edge case",
|
||||
]
|
||||
assert pr_review["head_sha"] == "aaaa1111bbbb2222"
|
||||
|
||||
def test_no_pr_review_field_when_none_present(self) -> None:
|
||||
t = _task(pr_number=8, commits=[{"sha": "abc", "message": "feat: x"}])
|
||||
t.notes_structured = None
|
||||
digest = build_task_handoff(t, [])
|
||||
assert digest is not None
|
||||
# Absent pr_review ⇒ no key (not a None-valued key) so a PM without a
|
||||
# prior gate verdict doesn't see a misleading empty slot.
|
||||
assert "pr_review" not in digest
|
||||
|
||||
def test_pr_review_alone_is_prior_work_worth_resuming(self) -> None:
|
||||
"""A task with no commits/dev-notes but a prior pr_fail verdict still
|
||||
surfaces the handoff so the owning PM reads the change-requests."""
|
||||
t = _task(pr_number=None, pr_url=None, dev_notes="")
|
||||
t.commits = []
|
||||
t.acceptance_criteria_status = []
|
||||
t.notes_structured = {
|
||||
"pr_review": {"verdict": "failed", "issues": ["fix the off-by-one"]}
|
||||
}
|
||||
digest = build_task_handoff(t, [])
|
||||
assert digest is not None
|
||||
assert digest["pr_review"]["issues"] == ["fix the off-by-one"]
|
||||
|
||||
Reference in New Issue
Block a user