diff --git a/tools/gate-check-v3/gate_check.py b/tools/gate-check-v3/gate_check.py index 87dbbd825..f10edb442 100644 --- a/tools/gate-check-v3/gate_check.py +++ b/tools/gate-check-v3/gate_check.py @@ -264,11 +264,18 @@ def signal_2_reviews(pr_number: int, repo: str) -> dict: blocking = [] for r in reviews: - if r.get("state") == "REQUEST_CHANGES" and not r.get("dismissed", False): + if ( + r.get("state") == "REQUEST_CHANGES" + and not r.get("dismissed", False) + and r.get("official") is not False + ): + login = (r.get("user") or {}).get("login", "") + if not login: + continue blocking.append( { "review_id": r["id"], - "user": r["user"]["login"], + "user": login, "commit_id": r.get("commit_id", ""), "created_at": r.get("submitted_at") or r.get("created_at", ""), } diff --git a/tools/gate-check-v3/test_gate_check.py b/tools/gate-check-v3/test_gate_check.py index 42b944c85..28da58adb 100644 --- a/tools/gate-check-v3/test_gate_check.py +++ b/tools/gate-check-v3/test_gate_check.py @@ -119,3 +119,55 @@ def test_signal_1_null_user_in_review_does_not_crash(monkeypatch): # Should not crash; the valid review from core-devops still satisfies engineers gate assert result["verdict"] == "CLEAR" assert result["results"]["core-devops"]["verdict"] == "APPROVED" + + +def test_signal_2_draft_request_changes_does_not_block(monkeypatch): + """official=False REQUEST_CHANGES is a draft/pending review and must NOT + block the gate (matching review-check.sh post-#1818 official-filter).""" + mod = load_gate_check() + + def fake_api_list(path): + if path == "/repos/molecule-ai/molecule-core/pulls/902/reviews": + return [ + { + "id": 1, + "user": {"login": "agent-reviewer"}, + "state": "REQUEST_CHANGES", + "official": False, + "dismissed": False, + "submitted_at": "2026-05-13T10:00:00Z", + } + ] + raise AssertionError(f"unexpected api_list: {path}") + + monkeypatch.setattr(mod, "api_list", fake_api_list) + + result = mod.signal_2_reviews(902, "molecule-ai/molecule-core") + assert result["verdict"] == "CLEAR" + assert result["blocking_reviews"] == [] + + +def test_signal_2_null_user_in_request_changes_does_not_crash(monkeypatch): + """Regression: Gitea may return user=null on a REQUEST_CHANGES review. + signal_2_reviews must survive this without AttributeError.""" + mod = load_gate_check() + + def fake_api_list(path): + if path == "/repos/molecule-ai/molecule-core/pulls/903/reviews": + return [ + { + "id": 1, + "user": None, + "state": "REQUEST_CHANGES", + "official": True, + "dismissed": False, + "submitted_at": "2026-05-13T10:00:00Z", + } + ] + raise AssertionError(f"unexpected api_list: {path}") + + monkeypatch.setattr(mod, "api_list", fake_api_list) + + result = mod.signal_2_reviews(903, "molecule-ai/molecule-core") + assert result["verdict"] == "CLEAR" + assert result["blocking_reviews"] == []