From 4a4e62ce5bc5d05f2ed058c377c8b3f4408c3937 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Thu, 4 Jun 2026 11:12:36 -0400 Subject: [PATCH] feat: make review budget caps env-configurable (PR_AF_MAX_DURATION_SECONDS / PR_AF_MAX_COST_USD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review duration/cost caps were only settable per-call via the review() reasoner args, which github-buddy does not pass — so every review used the hardcoded defaults (300s / $2). On non-trivial PRs the 300s cap is exhausted during anatomy/meta before review_dimension runs, yielding zero findings. Add a _resolve_budget_caps() helper: when the caller passes no explicit value, fall back to PR_AF_MAX_DURATION_SECONDS / PR_AF_MAX_COST_USD env vars, then to the historical 300s / $2 defaults. Defaults are unchanged when the env is unset; an explicit per-call arg still wins over the env. Mirrors the existing PR_AF_* env pattern in config.py. Co-Authored-By: Claude Opus 4.8 --- src/pr_af/app.py | 25 +++++++++++++++++++++++-- tests/test_budget_env.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/test_budget_env.py diff --git a/src/pr_af/app.py b/src/pr_af/app.py index b112704..0a3daad 100644 --- a/src/pr_af/app.py +++ b/src/pr_af/app.py @@ -59,6 +59,24 @@ def _extract_pr_number(pr_url: str) -> int | None: return None +def _resolve_budget_caps( + max_cost_usd: float | None, max_duration_seconds: int | None +) -> tuple[float, int]: + """Resolve the review budget caps. + + When the caller does not pass an explicit value, fall back to the + ``PR_AF_MAX_COST_USD`` / ``PR_AF_MAX_DURATION_SECONDS`` env vars (so the + budget can be tuned per deployment without a code change), and finally to + the historical defaults (2.0 USD / 300s) when neither is set. An explicit + argument always wins over the env var. + """ + if max_cost_usd is None: + max_cost_usd = float(os.getenv("PR_AF_MAX_COST_USD", "2.0")) + if max_duration_seconds is None: + max_duration_seconds = int(os.getenv("PR_AF_MAX_DURATION_SECONDS", "300")) + return max_cost_usd, max_duration_seconds + + def _checkout_pr_branch(target_dir: str, pr_number: int) -> None: git_env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": "echo"} # Fetch the PR head into FETCH_HEAD rather than directly into a local @@ -166,8 +184,8 @@ async def review( base_ref: str | None = None, head_ref: str | None = None, depth: str = "auto", - max_cost_usd: float = 2.0, - max_duration_seconds: int = 300, + max_cost_usd: float | None = None, + max_duration_seconds: int | None = None, focus: str = "auto", ignore_paths: list[str] | None = None, hints: list[str] | None = None, @@ -186,6 +204,9 @@ async def review( f"depth={depth!r}, dry_run={dry_run!r}", flush=True, ) + max_cost_usd, max_duration_seconds = _resolve_budget_caps( + max_cost_usd, max_duration_seconds + ) review_input = ReviewInput( pr_url=pr_url, diff_text=diff_text, diff --git a/tests/test_budget_env.py b/tests/test_budget_env.py new file mode 100644 index 0000000..7a09ec8 --- /dev/null +++ b/tests/test_budget_env.py @@ -0,0 +1,38 @@ +"""Tests for env-configurable review budget caps. + +Maps to the validation contract for ``_resolve_budget_caps``: + +* caller passes no value + env set -> env value is used +* caller passes no value + env unset -> historical defaults (2.0 USD / 300s) +* caller passes an explicit value -> it wins over the env var +""" + +from __future__ import annotations + +import pytest + +from pr_af.app import _resolve_budget_caps + + +def test_env_overrides_when_no_explicit_value(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PR_AF_MAX_DURATION_SECONDS", "1800") + monkeypatch.setenv("PR_AF_MAX_COST_USD", "5") + cost, duration = _resolve_budget_caps(None, None) + assert duration == 1800 + assert cost == 5.0 + + +def test_defaults_when_env_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PR_AF_MAX_DURATION_SECONDS", raising=False) + monkeypatch.delenv("PR_AF_MAX_COST_USD", raising=False) + cost, duration = _resolve_budget_caps(None, None) + assert duration == 300 + assert cost == 2.0 + + +def test_explicit_value_wins_over_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PR_AF_MAX_DURATION_SECONDS", "1800") + monkeypatch.setenv("PR_AF_MAX_COST_USD", "5") + cost, duration = _resolve_budget_caps(7.0, 900) + assert duration == 900 + assert cost == 7.0