feat: add review effort accounting and budget governor
This commit is contained in:
@@ -429,6 +429,41 @@ def test_parse_repo_config_static_message_ignores_blank():
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": 42}))
|
||||
|
||||
|
||||
def test_parse_repo_config_sanitizes_budget_limits():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"budget": {
|
||||
"max_steps": "20",
|
||||
"max_total_tokens": 120000,
|
||||
"max_output_tokens": 20000,
|
||||
"max_duration_seconds": 480,
|
||||
"max_lenses": 4,
|
||||
"max_equivalent_cost_usd": "1.25",
|
||||
"unknown": 99,
|
||||
}
|
||||
}))
|
||||
assert cfg["budget"] == {
|
||||
"max_steps": 20,
|
||||
"max_total_tokens": 120000,
|
||||
"max_output_tokens": 20000,
|
||||
"max_duration_seconds": 480,
|
||||
"max_lenses": 4,
|
||||
"max_equivalent_cost_usd": 1.25,
|
||||
}
|
||||
|
||||
|
||||
def test_parse_repo_config_drops_invalid_budget_values():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"budget": {
|
||||
"max_steps": 0,
|
||||
"max_total_tokens": 999999999,
|
||||
"max_duration_seconds": -1,
|
||||
"max_lenses": 99,
|
||||
"max_equivalent_cost_usd": 0,
|
||||
}
|
||||
}))
|
||||
assert "budget" not in cfg
|
||||
|
||||
|
||||
def test_parse_repo_config_reads_model_override():
|
||||
# Per-repo override is validated against cost_model.PRICES. Only keys
|
||||
# the cost model knows about can override the review engine.
|
||||
@@ -2148,4 +2183,3 @@ def test_format_review_body_confidence_clamps_out_of_range():
|
||||
assert "Merge confidence: 5/5 🟢" in body_hi
|
||||
body_lo = format_review_body("- x", "glm-5.2:cloud", "abcdef1234567890", confidence=0)
|
||||
assert "Merge confidence: 1/5 🔴" in body_lo
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Budget policy and accounting tests."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
sys.path.insert(0, os.path.join(ROOT, "pilot"))
|
||||
|
||||
from review.budget import Budget, BudgetState # noqa: E402
|
||||
from review import opencode_runtime # noqa: E402
|
||||
from review.opencode import parse_opencode_events # noqa: E402
|
||||
|
||||
|
||||
def test_budget_reads_config_over_environment(monkeypatch):
|
||||
monkeypatch.setenv("PRAGENT_MAX_REVIEW_STEPS", "3")
|
||||
budget = Budget.from_config({"budget": {"max_steps": 7}})
|
||||
assert budget.max_steps == 7
|
||||
|
||||
|
||||
def test_budget_state_stops_at_token_limit():
|
||||
state = BudgetState(Budget(max_steps=20, max_total_tokens=100))
|
||||
assert state.record({"steps": 1, "total": 60, "output": 10}) == ""
|
||||
assert state.record({"steps": 1, "total": 40, "output": 10}) == "max_total_tokens"
|
||||
assert state.snapshot()["cap_hit"] is True
|
||||
|
||||
|
||||
def test_budget_state_tracks_cost_cap():
|
||||
state = BudgetState(Budget(max_equivalent_cost_usd=1.0))
|
||||
assert state.record({"steps": 1, "total": 1}, 0.75) == ""
|
||||
assert state.record({"steps": 1, "total": 1}, 0.25) == "max_equivalent_cost_usd"
|
||||
|
||||
|
||||
def test_process_terminates_after_step_budget():
|
||||
code = (
|
||||
"import json,time; "
|
||||
"print(json.dumps({'type':'step_finish','part':{'tokens':{"
|
||||
"'input':1,'output':1,'total':2}}}), flush=True); "
|
||||
"time.sleep(30)"
|
||||
)
|
||||
budget = Budget(max_steps=1, max_duration_seconds=10)
|
||||
state = BudgetState(budget)
|
||||
proc = opencode_runtime._run_process(
|
||||
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
|
||||
timeout=10, parse_events=parse_opencode_events, budget=budget,
|
||||
budget_state=state, model="glm-5.2:cloud", runner=None,
|
||||
)
|
||||
assert state.snapshot()["cap_reason"] == "max_steps"
|
||||
assert proc.stdout.count("step_finish") == 1
|
||||
@@ -201,6 +201,13 @@ def test_parse_events_text_and_usage_summed():
|
||||
assert usage["cache_write"] == 1
|
||||
assert usage["total"] == 150
|
||||
assert abs(usage["cost"] - 0.01) < 1e-9
|
||||
assert usage["tool_calls"] == 0
|
||||
assert usage["iterations"] == [
|
||||
{"step": 1, "input": 90, "output": 10, "reasoning": 0,
|
||||
"cache_read": 5, "cache_write": 0, "total": 100, "cost": 0.0},
|
||||
{"step": 2, "input": 40, "output": 10, "reasoning": 2,
|
||||
"cache_read": 0, "cache_write": 1, "total": 50, "cost": 0.01},
|
||||
]
|
||||
|
||||
|
||||
def test_parse_events_no_step_finish_returns_none_usage():
|
||||
@@ -481,4 +488,3 @@ def test_committed_config_has_no_private_address():
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-lens orchestration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user