diff --git a/src/gitea_codex_bot/services/reviewer.py b/src/gitea_codex_bot/services/reviewer.py index c3a8b03..482dc98 100644 --- a/src/gitea_codex_bot/services/reviewer.py +++ b/src/gitea_codex_bot/services/reviewer.py @@ -166,8 +166,42 @@ def _call_openai_review(settings: Settings, prompt: str) -> dict[str, Any]: raise ReviewError("OpenAI response did not contain JSON output text.") -def _fallback_review(diff_context: dict[str, Any]) -> dict[str, Any]: - findings = [] +def _summarize_openai_failure(exc: Exception) -> str: + if isinstance(exc, httpx.HTTPStatusError): + status = exc.response.status_code + response_text = exc.response.text.strip() + if response_text: + compact = " ".join(response_text.split()) + if len(compact) > 400: + compact = f"{compact[:400]}..." + return f"OpenAI API HTTP {status}: {compact}" + return f"OpenAI API HTTP {status}." + if isinstance(exc, httpx.TimeoutException): + return "OpenAI API request timed out." + message = str(exc).strip() + if message: + return message + return f"{exc.__class__.__name__} (no details)" + + +def _fallback_review(diff_context: dict[str, Any], *, failure_reason: str | None = None) -> dict[str, Any]: + findings: list[dict[str, Any]] = [] + summary = "Fallback analysis was used because OpenAI review was unavailable." + + if failure_reason: + summary = f"OpenAI review failed. Error: {failure_reason}" + findings.append( + { + "severity": "high", + "file": "unknown", + "line_start": 1, + "line_end": 1, + "title": "OpenAI review request failed", + "body": failure_reason, + "suggestion": "Fix API/auth/network issues and rerun @codex review.", + } + ) + if "TODO" in diff_context["diff"]: findings.append( { @@ -183,7 +217,7 @@ def _fallback_review(diff_context: dict[str, Any]) -> dict[str, Any]: return { "verdict": "correct" if not findings else "has_issues", "confidence": 0.4 if not findings else 0.6, - "summary": "Fallback analysis was used because OpenAI review was unavailable.", + "summary": summary, "findings": findings, } @@ -198,8 +232,8 @@ def run_review_for_pr( prompt, diff_context, repo_cfg = prepare_review_prompt(settings, gitea, repo, pr_number, command) try: result = _call_openai_review(settings, prompt) - except Exception: - result = _fallback_review(diff_context) + except Exception as exc: + result = _fallback_review(diff_context, failure_reason=_summarize_openai_failure(exc)) return normalize_review_result(result), repo_cfg diff --git a/tests/test_reviewer_fallback.py b/tests/test_reviewer_fallback.py new file mode 100644 index 0000000..9a818e6 --- /dev/null +++ b/tests/test_reviewer_fallback.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import httpx + +from gitea_codex_bot.config import get_settings +from gitea_codex_bot.services.repo_config import RepoReviewConfig +from gitea_codex_bot.services.reviewer import _fallback_review, run_review_for_pr +from gitea_codex_bot.types import ParsedCommand + + +def test_fallback_review_surfaces_failure_reason() -> None: + result = _fallback_review({"diff": ""}, failure_reason="OpenAI API HTTP 401: invalid_api_key") + + assert result["verdict"] == "has_issues" + assert result["summary"] == "OpenAI review failed. Error: OpenAI API HTTP 401: invalid_api_key" + assert result["findings"][0]["title"] == "OpenAI review request failed" + assert result["findings"][0]["body"] == "OpenAI API HTTP 401: invalid_api_key" + + +def test_run_review_for_pr_uses_openai_http_error_in_fallback(monkeypatch) -> None: + def _fake_prepare(*_args, **_kwargs): + return "prompt", {"diff": "TODO: tighten validation"}, RepoReviewConfig() + + def _raise_http_error(*_args, **_kwargs): + request = httpx.Request("POST", "https://api.openai.com/v1/responses") + response = httpx.Response(429, request=request, text='{"error":{"message":"rate_limited"}}') + raise httpx.HTTPStatusError("rate limited", request=request, response=response) + + monkeypatch.setattr("gitea_codex_bot.services.reviewer.prepare_review_prompt", _fake_prepare) + monkeypatch.setattr("gitea_codex_bot.services.reviewer._call_openai_review", _raise_http_error) + + settings = get_settings() + command = ParsedCommand(name="review", raw="@codex review") + result, _repo_cfg = run_review_for_pr(settings, object(), "acme/repo", 9, command) + + assert result["summary"].startswith("OpenAI review failed. Error: OpenAI API HTTP 429:") + assert result["findings"][0]["title"] == "OpenAI review request failed" + assert "rate_limited" in result["findings"][0]["body"] + assert any(finding["title"] == "TODO marker in diff" for finding in result["findings"])