From 7db4e388e47ffcb9fcc5adbeb8dafe45a80b4ea4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:17:23 +0900 Subject: [PATCH 1/3] fix: retry reviews after coverage blockers clear --- scripts/ci/pr_review_merge_scheduler.py | 54 ++++++++++++++++ tests/test_pr_review_merge_scheduler.py | 86 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 75e18c860c..1717b5b0cb 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -158,6 +158,11 @@ "deterministic fallback approval", "did not emit a usable current-head control block", ) +COVERAGE_REVIEW_MARKERS = ( + "coverage evidence did not pass", + "coverage-evidence", + "test evidence: not proven passing", +) LAST_PUSH_APPROVAL_RESTAMP_MESSAGE = "chore: refresh head for last-push approval" @@ -1193,6 +1198,33 @@ def has_current_head_changes_requested(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "CHANGES_REQUESTED") +def current_head_coverage_change_request(pr: dict[str, Any]) -> bool: + """Return whether the latest current-head request is only a coverage gate.""" + for review in reversed((pr.get("reviews") or {}).get("nodes") or []): + if not is_opencode_review(review) or not review_matches_current_head(review, pr): + continue + if (review.get("state") or "").upper() != "CHANGES_REQUESTED": + return False + body = (review.get("body") or "").lower() + return all(marker in body for marker in COVERAGE_REVIEW_MARKERS) + return False + + +def coverage_evidence_state(pr: dict[str, Any]) -> str: + """Return missing, running, complete, or failed for the latest coverage gate.""" + for node in reversed(context_nodes(pr)): + name = (node.get("name") or node.get("context") or "").lower() + if name != "coverage-evidence": + continue + status = (node.get("status") or node.get("state") or "").upper() + if status in RUNNING_CHECK_STATES: + return "running" + if node.get("__typename") == "CheckRun": + return "complete" if (node.get("conclusion") or "").upper() == "SUCCESS" else "failed" + return "complete" if status == "SUCCESS" else "failed" + return "missing" + + def stale_opencode_change_request_ids(pr: dict[str, Any]) -> list[int]: """Return dismissible automated change requests tied to previous heads.""" review_ids: list[int] = [] @@ -2388,6 +2420,28 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio return decide("block", f"{unresolved} unresolved review thread(s)") if has_current_head_changes_requested(pr): + coverage_ready = ( + trigger_reviews + and review_dispatch_allowed + and current_head_coverage_change_request(pr) + and coverage_evidence_state(pr) == "complete" + and strix_evidence_state(pr) == "complete" + and not failed_status_checks(pr) + ) + if coverage_ready: + wait_reason = repository_dispatch_wait_reason(repo, workflow) + if wait_reason: + return decide("wait", wait_reason) + dispatch_result = dispatch_opencode_review(repo, workflow, pr, dry_run=dry_run) + if dispatch_result == "already_running": + return decide( + "wait", + "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active", + ) + return decide( + "review_dispatch", + "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched", + ) if pr.get("autoMergeRequest"): return finish( disable_auto_merge_decision( diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 3e421e9039..2872c90a48 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1239,6 +1239,51 @@ def test_review_state_and_failed_checks(): assert sched.has_current_head_approval(superseded) assert not sched.has_current_head_changes_requested(superseded) + coverage_request = make_pr( + reviews={ + "nodes": [ + { + **opencode_review("CHANGES_REQUESTED", "head"), + "body": ( + "OpenCode cannot approve yet because required coverage evidence did not pass. " + "The coverage-evidence gate reported test evidence: not proven passing." + ), + } + ] + }, + statusCheckRollup={ + "contexts": { + "nodes": [ + strix_check(), + { + "__typename": "CheckRun", + "name": "coverage-evidence", + "status": "COMPLETED", + "conclusion": "SUCCESS", + }, + ] + } + }, + ) + assert sched.current_head_coverage_change_request(coverage_request) + assert sched.coverage_evidence_state(coverage_request) == "complete" + assert sched.coverage_evidence_state( + make_pr( + statusCheckRollup={ + "contexts": {"nodes": [{"name": "coverage-evidence", "state": "PENDING"}]} + } + ) + ) == "running" + assert sched.coverage_evidence_state(make_pr()) == "missing" + ordinary_request = make_pr( + reviews={ + "nodes": [ + {**opencode_review("CHANGES_REQUESTED", "head"), "body": "Fix the estimator."} + ] + } + ) + assert not sched.current_head_coverage_change_request(ordinary_request) + stale_gate_reviews = make_pr( reviews={ "nodes": [ @@ -2997,6 +3042,47 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): assert inspect(make_pr(reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "head")]})).reason == ( "current-head OpenCode review requested changes" ) + coverage_request = make_pr( + reviews={ + "nodes": [ + { + **opencode_review("CHANGES_REQUESTED", "head"), + "body": ( + "OpenCode cannot approve yet because required coverage evidence did not pass. " + "The coverage-evidence gate reported test evidence: not proven passing." + ), + } + ] + }, + statusCheckRollup={ + "contexts": { + "nodes": [ + strix_check(), + { + "__typename": "CheckRun", + "name": "coverage-evidence", + "status": "COMPLETED", + "conclusion": "SUCCESS", + }, + ] + } + }, + ) + dispatched = [] + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatched.append( + (repo, workflow, pr["headRefOid"], dry_run) + ) + or "dispatched", + ) + coverage_decision = inspect(coverage_request) + assert coverage_decision.action == "review_dispatch" + assert coverage_decision.reason == ( + "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched" + ) + assert dispatched == [("owner/repo", "OpenCode Review", "head", True)] action_required_pr = make_pr( statusCheckRollup={ "contexts": { From 53b5cef0ddadf4a2666b6fa0812240e9ca571103 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:30:48 +0900 Subject: [PATCH 2/3] fix(scheduler): match coverage blocker review body --- scripts/ci/pr_review_merge_scheduler.py | 2 +- tests/test_pr_review_merge_scheduler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 1717b5b0cb..46527b12c1 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -161,7 +161,7 @@ COVERAGE_REVIEW_MARKERS = ( "coverage evidence did not pass", "coverage-evidence", - "test evidence: not proven passing", + "required test/docstring evidence", ) LAST_PUSH_APPROVAL_RESTAMP_MESSAGE = "chore: refresh head for last-push approval" diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 2872c90a48..713c21fc4f 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1246,7 +1246,7 @@ def test_review_state_and_failed_checks(): **opencode_review("CHANGES_REQUESTED", "head"), "body": ( "OpenCode cannot approve yet because required coverage evidence did not pass. " - "The coverage-evidence gate reported test evidence: not proven passing." + "The coverage-evidence gate reported that required test/docstring evidence was not proven." ), } ] @@ -3049,7 +3049,7 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): **opencode_review("CHANGES_REQUESTED", "head"), "body": ( "OpenCode cannot approve yet because required coverage evidence did not pass. " - "The coverage-evidence gate reported test evidence: not proven passing." + "The coverage-evidence gate reported that required test/docstring evidence was not proven." ), } ] From c041c34998db91da2ed21606e6b14ab65339a76a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 21:52:33 +0900 Subject: [PATCH 3/3] test(scheduler): cover coverage-blocker dispatch branches --- tests/test_pr_review_merge_scheduler.py | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 713c21fc4f..7ad4a8308c 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1274,6 +1274,18 @@ def test_review_state_and_failed_checks(): } ) ) == "running" + assert sched.coverage_evidence_state( + make_pr( + statusCheckRollup={ + "contexts": { + "nodes": [ + {"name": "coverage-evidence", "state": "FAILURE"}, + {"name": "unrelated-check", "state": "SUCCESS"}, + ] + } + } + ) + ) == "failed" assert sched.coverage_evidence_state(make_pr()) == "missing" ordinary_request = make_pr( reviews={ @@ -1283,6 +1295,12 @@ def test_review_state_and_failed_checks(): } ) assert not sched.current_head_coverage_change_request(ordinary_request) + assert not sched.current_head_coverage_change_request( + make_pr(reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "old")]}) + ) + assert not sched.current_head_coverage_change_request( + make_pr(reviews={"nodes": [opencode_review("APPROVED", "head")]}) + ) stale_gate_reviews = make_pr( reviews={ @@ -3083,6 +3101,23 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched" ) assert dispatched == [("owner/repo", "OpenCode Review", "head", True)] + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda repo, workflow: "another scheduler dispatch is active", + ) + assert inspect(coverage_request).action == "wait" + monkeypatch.setattr(sched, "repository_dispatch_wait_reason", lambda repo, workflow: None) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: "already_running", + ) + already_running = inspect(coverage_request) + assert already_running.action == "wait" + assert already_running.reason == ( + "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active" + ) action_required_pr = make_pr( statusCheckRollup={ "contexts": {