Merge agent-B: cost-line fix + static_message + per-repo model override
# Conflicts: # pilot/ai_review.py # tests/pilot/test_ai_review.py
This commit is contained in:
@@ -410,6 +410,81 @@ def test_parse_repo_config_partial_and_bad():
|
||||
assert parse_repo_config('{"instructions":" "}') == {"enabled": False}
|
||||
|
||||
|
||||
def test_parse_repo_config_reads_static_message():
|
||||
cfg = parse_repo_config(json.dumps({"static_message": " NOTE: this repo is in maintenance mode "}))
|
||||
assert cfg.get("static_message") == "NOTE: this repo is in maintenance mode"
|
||||
assert cfg.get("enabled") is False
|
||||
|
||||
|
||||
def test_parse_repo_config_static_message_caps_length():
|
||||
long_text = "x" * 9999
|
||||
cfg = parse_repo_config(json.dumps({"static_message": long_text}))
|
||||
assert "static_message" in cfg
|
||||
assert len(cfg["static_message"]) <= 400
|
||||
|
||||
|
||||
def test_parse_repo_config_static_message_ignores_blank():
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": " "}))
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": ""}))
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": 42}))
|
||||
|
||||
|
||||
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.
|
||||
cfg = parse_repo_config(json.dumps({"model": "claude-sonnet-5"}))
|
||||
assert cfg.get("model") == "claude-sonnet-5"
|
||||
|
||||
|
||||
def test_parse_repo_config_rejects_unknown_model(capsys):
|
||||
cfg = parse_repo_config(json.dumps({"model": "not-in-prices"}))
|
||||
assert "model" not in cfg
|
||||
# Repos that pin a typo should get a stderr hint pointing at the valid set.
|
||||
err = capsys.readouterr().err
|
||||
assert "model" in err.lower() or "prices" in err.lower() or "unknown" in err.lower()
|
||||
|
||||
|
||||
def test_parse_repo_config_model_must_be_string():
|
||||
assert "model" not in parse_repo_config(json.dumps({"model": 42}))
|
||||
assert "model" not in parse_repo_config(json.dumps({"model": []}))
|
||||
assert "model" not in parse_repo_config(json.dumps({"model": None}))
|
||||
|
||||
|
||||
def test_resolve_display_model_precedence(monkeypatch):
|
||||
# Order is OPENCODE_MODEL env > config['model'] > headroom/{base}.
|
||||
monkeypatch.delenv("OPENCODE_MODEL", raising=False)
|
||||
# 1. No env, no config → headroom/<base>
|
||||
assert ai_review._resolve_display_model("MiniMax-M2.7", None) == "headroom/MiniMax-M2.7"
|
||||
assert ai_review._resolve_display_model("MiniMax-M2.7", {}) == "headroom/MiniMax-M2.7"
|
||||
# 2. No env, config has model → use config model as-is (already a known key)
|
||||
assert (
|
||||
ai_review._resolve_display_model("MiniMax-M2.7", {"model": "claude-sonnet-5"})
|
||||
== "claude-sonnet-5"
|
||||
)
|
||||
# 3. Env wins over config
|
||||
monkeypatch.setenv("OPENCODE_MODEL", "headroom/MiniMax-M2.7")
|
||||
assert (
|
||||
ai_review._resolve_display_model("MiniMax-M2.7", {"model": "claude-sonnet-5"})
|
||||
== "headroom/MiniMax-M2.7"
|
||||
)
|
||||
# 4. Env alone, no config
|
||||
monkeypatch.delenv("OPENCODE_MODEL")
|
||||
assert ai_review._resolve_display_model("x", {}) == "headroom/x"
|
||||
|
||||
|
||||
def test_format_review_body_uses_override_for_cost_paren():
|
||||
# End-to-end sanity: when the caller passes the resolved override as the
|
||||
# `model` arg to format_review_body, both the header AND the cost line
|
||||
# show the override — i.e. callers DO substitute the resolved display
|
||||
# name into both the opencode subprocess ref and the review body.
|
||||
body = format_review_body(
|
||||
"- [high] x:1 — bug. fix.", "claude-sonnet-5", "abcdef1234567890",
|
||||
)
|
||||
assert "claude-sonnet-5" in body
|
||||
assert "MiniMax-M2.7" not in body # the base didn't leak through
|
||||
assert "🤖" in body # header rendered
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dedupe / prior-context parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -789,6 +864,39 @@ def test_render_collapsible_usage_cost_nonzero_drops_free_tier_note():
|
||||
"cache_write": 0, "total": 10, "cost": 0.0123, "steps": 1, "duration_s": 1.0}
|
||||
sec = _render_collapsible_usage(usage, "m", config=None)
|
||||
assert "$0.0123" in sec
|
||||
# Was hardcoded "free tier" previously; now says "billed" since cost > 0.
|
||||
assert "billed" in sec
|
||||
assert "free tier" not in sec
|
||||
|
||||
|
||||
def test_render_collapsible_usage_uses_passed_model_for_free_tier_clause():
|
||||
# Regression: the cost parenthetical must reflect the actually-routed model,
|
||||
# not a stale hardcoded `headroom glm-5.2:cloud` literal that predates the
|
||||
# MiniMax / Anthropic switch.
|
||||
usage = {"input": 10, "output": 0, "reasoning": 0, "cache_read": 0,
|
||||
"cache_write": 0, "total": 10, "cost": 0.0, "steps": 1, "duration_s": 1.0}
|
||||
sec = _render_collapsible_usage(usage, "MiniMax-M2.7", config=None)
|
||||
# The parenthetical clause is "(<model> — free tier)" — a model name MUST
|
||||
# sit immediately before "— free tier".
|
||||
assert "(MiniMax-M2.7 — free tier)" in sec
|
||||
# And the stale hardcoded model name must no longer appear anywhere.
|
||||
assert "glm-5.2:cloud" not in sec
|
||||
|
||||
|
||||
def test_render_collapsible_usage_full_provider_prefix_in_display():
|
||||
# When the caller has resolved a provider-prefixed model ref (the opencode
|
||||
# subprocess path), the parenthetical should mirror that verbatim.
|
||||
usage = {"input": 10, "output": 0, "reasoning": 0, "cache_read": 0,
|
||||
"cache_write": 0, "total": 10, "cost": 0.0, "steps": 1, "duration_s": 1.0}
|
||||
sec = _render_collapsible_usage(usage, "headroom/MiniMax-M2.7", config=None)
|
||||
assert "(headroom/MiniMax-M2.7 — free tier)" in sec
|
||||
|
||||
|
||||
def test_render_collapsible_usage_nonzero_cost_says_billed():
|
||||
usage = {"input": 10, "output": 0, "reasoning": 0, "cache_read": 0,
|
||||
"cache_write": 0, "total": 10, "cost": 0.123, "steps": 1, "duration_s": 1.0}
|
||||
sec = _render_collapsible_usage(usage, "MiniMax-M2.7", config=None)
|
||||
assert "(MiniMax-M2.7 — billed)" in sec
|
||||
assert "free tier" not in sec
|
||||
|
||||
|
||||
@@ -808,6 +916,22 @@ def test_format_review_body_no_usage_section_omitted():
|
||||
assert "AI usage" not in body
|
||||
|
||||
|
||||
def test_format_review_body_renders_static_message_banner():
|
||||
body = format_review_body(
|
||||
"", "glm-5.2:cloud", "abcdef1234567890",
|
||||
static_message="NOTE: this repo is in maintenance mode.",
|
||||
)
|
||||
assert "> NOTE: this repo is in maintenance mode." in body
|
||||
# Banner sits under the header and above the rest of the body.
|
||||
assert body.index("NOTE") > body.index("🤖")
|
||||
assert body.index("NOTE") < body.index("### Summary of Changes")
|
||||
|
||||
|
||||
def test_format_review_body_omits_static_message_when_blank():
|
||||
body = format_review_body("", "glm-5.2:cloud", "abcdef1234567890")
|
||||
assert "> " not in body
|
||||
|
||||
|
||||
def test_format_review_body_with_summary_changes_and_risks():
|
||||
body = format_review_body(
|
||||
"", "glm-5.2:cloud", "abcdef1234567890",
|
||||
@@ -1141,7 +1265,11 @@ def test_usage_block_shows_equivalent_provider_cost():
|
||||
# grok-4.5; cost_target defaults to sonnet-5 (bolded).
|
||||
assert "🔋 AI Usage & Run Details" in sec
|
||||
assert "**Actual**: $0.00" in sec
|
||||
# The "free tier" clause must mention the routed model verbatim, not the
|
||||
# stale hardcoded `headroom glm-5.2:cloud` literal.
|
||||
assert "free tier" in sec
|
||||
assert "glm-5.2:cloud" in sec
|
||||
# Equivalent should be > 0 for non-trivial token counts.
|
||||
assert "$0.00" in sec # the actual line
|
||||
# Multi-provider table header present, default roster rendered, default
|
||||
# cost_target (Sonnet 5) is the bolded row.
|
||||
|
||||
Reference in New Issue
Block a user