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