mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
fix: publish_review tool returns generic "Failed to POST PR review" without GitHub API status/body, agent retries with no signal (#1299)
* fix(reviewer): surface HTTP status and body excerpt for non-dict GitHub PR review responses
When post_pull_request_review received a non-dict body, it returned
None and publish_review surfaced a generic 'Failed to POST PR review'
string with no signal for the agent to adapt — leading to blind
retries with permuted cap/severity_threshold args.
Now the non-dict-body path mirrors the existing HTTPStatusError /
HTTPError paths: it returns {'_error': 'HTTP <status>: non-dict
response body: <excerpt>'} so the user-facing tool can include the
underlying detail. The bare-None branch in publish_review.py is kept
as a defensive guard with a clearer message.
* ci: apply ruff format to reviewer_publish.py
Collapse the multi-line return dict into a single line so it matches the
output of `ruff format`, unblocking the Agent lint / format-check CI jobs.
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
---------
Co-authored-by: LangSmith Issues Agent <issues-agent@langsmith.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
This commit is contained in:
parent
d2c62e5fc6
commit
74f5df5df8
3 changed files with 55 additions and 3 deletions
|
|
@ -144,7 +144,17 @@ async def post_pull_request_review(
|
|||
logger.exception("Failed to POST PR review for %s/%s#%s", owner, repo, pr_number)
|
||||
return {"_error": f"{type(e).__name__}: {e}"}
|
||||
data = response.json()
|
||||
return data if isinstance(data, dict) else None
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
body_excerpt = (response.text or "")[:500]
|
||||
logger.error(
|
||||
"POST PR review for %s/%s#%s returned non-dict body: %s",
|
||||
owner,
|
||||
repo,
|
||||
pr_number,
|
||||
body_excerpt,
|
||||
)
|
||||
return {"_error": (f"HTTP {response.status_code}: non-dict response body: {body_excerpt}")}
|
||||
|
||||
|
||||
async def fetch_review_comments(
|
||||
|
|
|
|||
|
|
@ -174,13 +174,18 @@ async def _publish_review_async(
|
|||
inline_comments=inline_comments,
|
||||
token=token,
|
||||
)
|
||||
if review_response is None:
|
||||
return {"success": False, "error": "Failed to POST PR review"}
|
||||
if isinstance(review_response, dict) and "_error" in review_response:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to POST PR review: {review_response['_error']}",
|
||||
}
|
||||
if review_response is None:
|
||||
# Defensive guard: with the upstream change this should never happen,
|
||||
# but keep a clear signal if it does so the agent doesn't retry blindly.
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Failed to POST PR review: no response from GitHub",
|
||||
}
|
||||
review_id = review_response.get("id") if isinstance(review_response, dict) else None
|
||||
|
||||
if review_id is not None and inline_comments:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import pytest
|
|||
|
||||
from agent.reviewer_findings import Finding, new_finding
|
||||
from agent.reviewer_publish import (
|
||||
post_pull_request_review,
|
||||
render_inline_comment_body,
|
||||
render_inline_comment_payload,
|
||||
render_review_body,
|
||||
|
|
@ -102,6 +103,42 @@ async def test_resolve_review_thread_returns_true_on_success() -> None:
|
|||
assert ok is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_pull_request_review_non_dict_body_surfaces_status_and_excerpt() -> None:
|
||||
"""A non-dict GitHub response body must surface status code + body excerpt
|
||||
via ``_error`` rather than collapsing to a bare ``None`` (which the
|
||||
user-facing tool would render as the unhelpful ``Failed to POST PR review``)."""
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = ["unexpected", "list", "body"]
|
||||
response.text = '["unexpected", "list", "body"]'
|
||||
response.raise_for_status.return_value = None
|
||||
|
||||
client_cm = AsyncMock()
|
||||
client_cm.__aenter__.return_value = client_cm
|
||||
client_cm.post = AsyncMock(return_value=response)
|
||||
|
||||
with patch("agent.reviewer_publish.httpx.AsyncClient", return_value=client_cm):
|
||||
result = await post_pull_request_review(
|
||||
owner="o",
|
||||
repo="r",
|
||||
pr_number=1,
|
||||
head_sha="sha",
|
||||
body="b",
|
||||
inline_comments=[],
|
||||
token="t",
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert "_error" in result
|
||||
err = result["_error"]
|
||||
assert "HTTP 200" in err
|
||||
assert "non-dict" in err
|
||||
assert "unexpected" in err
|
||||
# The bare legacy string must not be the only signal anymore.
|
||||
assert err != "Failed to POST PR review"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_review_thread_returns_false_on_graphql_errors() -> None:
|
||||
response = MagicMock()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue