refactor: split opencode runtime and tests
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Unit tests for the opencode engine glue (no network, no opencode run)."""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, os.path.join(ROOT, "pilot"))
|
||||
|
||||
import opencode_review as oc # noqa: E402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# write_brief
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_no_surface_response_parses_as_an_empty_review():
|
||||
# The skip path must return the same shape every other path returns.
|
||||
# A bare "" landed in ai_review's unparseable-output branch and posted
|
||||
# "AI review produced no parseable output" — a malfunction, not a verdict.
|
||||
import ai_review
|
||||
text, usage = oc._no_surface_response("o/r", "9", "abc12345", 3)
|
||||
assert usage is None
|
||||
summary, findings, _changes, _risks, _walkthrough, _risk_verdict, _test_coverage = (
|
||||
ai_review.parse_review_output(text)
|
||||
)
|
||||
assert findings == []
|
||||
assert summary # non-empty, so ai_review does NOT take the salvage branch
|
||||
assert "no review surface" in summary.lower()
|
||||
assert "3 configured lens" in summary
|
||||
|
||||
|
||||
def test_no_surface_response_zero_lenses_wording():
|
||||
import ai_review
|
||||
text, _ = oc._no_surface_response("o/r", "9", "abc12345", 0)
|
||||
summary, findings, _c, _r, _w, _rv, _tc = ai_review.parse_review_output(text)
|
||||
assert findings == []
|
||||
assert "after path filtering" in summary
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _synthesize_summary_fields — Task 8: real Python fallback implementation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_synthesize_walkthrough_groups_findings_by_path():
|
||||
findings = [
|
||||
{"path": "a.py", "line": 1, "severity": "medium", "problem": "fix x"},
|
||||
{"path": "b.py", "line": 2, "severity": "high", "problem": "fix y"},
|
||||
]
|
||||
w, _, _ = oc._synthesize_summary_fields(findings, "")
|
||||
assert any("a.py" in line for line in w)
|
||||
assert any("b.py" in line for line in w)
|
||||
|
||||
|
||||
def test_synthesize_walkthrough_empty_when_no_findings_uses_changed_files():
|
||||
w, _, _ = oc._synthesize_summary_fields(
|
||||
[],
|
||||
"diff --git a/x.py b/x.py\n@@ -1 +1 @@\n-old\n+new\n+++ b/x.py\n",
|
||||
)
|
||||
assert any("x.py" in line for line in w)
|
||||
|
||||
|
||||
def test_synthesize_risk_verdict_critical():
|
||||
findings = [{"severity": "critical"}]
|
||||
_, rv, _ = oc._synthesize_summary_fields(findings, "")
|
||||
assert "Critical risk" in rv
|
||||
|
||||
|
||||
def test_synthesize_risk_verdict_clean():
|
||||
_, rv, _ = oc._synthesize_summary_fields([], "")
|
||||
assert "Low risk" in rv
|
||||
|
||||
|
||||
def test_synthesize_test_coverage_with_test_path():
|
||||
_, _, tc = oc._synthesize_summary_fields(
|
||||
[], "+diff\n", changed_paths=["pilot/foo.py", "tests/test_foo.py"])
|
||||
assert tc == "Tests changed"
|
||||
|
||||
|
||||
def test_synthesize_test_coverage_missing_tests():
|
||||
_, _, tc = oc._synthesize_summary_fields(
|
||||
[], "+diff\n", changed_paths=["pilot/foo.py"])
|
||||
assert "No tests for behavioral change" in tc
|
||||
|
||||
|
||||
def test_synthesize_walkthrough_picks_peak_severity_per_path():
|
||||
# Three findings on the same path, with mixed severities. The walkthrough
|
||||
# headline should use the PEAK severity's emoji (critical = 🔴), not the
|
||||
# lexicographic-first severity (low).
|
||||
findings = [
|
||||
{"path": "x.py", "line": 1, "severity": "low",
|
||||
"problem": "minor nit"},
|
||||
{"path": "x.py", "line": 5, "severity": "critical",
|
||||
"problem": "sql injection"},
|
||||
{"path": "x.py", "line": 9, "severity": "high",
|
||||
"problem": "auth bypass"},
|
||||
]
|
||||
w, _, _ = oc._synthesize_summary_fields(findings, "")
|
||||
assert len(w) == 1
|
||||
line = w[0]
|
||||
assert "`x.py`" in line
|
||||
assert "🔴" in line # critical = 🔴
|
||||
assert "🟡" not in line
|
||||
assert "🔵" not in line
|
||||
assert "sql injection" in line # critical finding's problem, not low's
|
||||
|
||||
|
||||
def test_synthesize_summary_fields_none_findings_safe():
|
||||
# Old code crashed in risk_verdict with `for f in findings:` on None.
|
||||
# After the `findings = findings or []` guard, None behaves like [].
|
||||
w, rv, tc = oc._synthesize_summary_fields(None, "")
|
||||
assert isinstance(w, list)
|
||||
assert rv.startswith("Low risk")
|
||||
# walkthrough should fall through to the diff-derived path list — empty
|
||||
# diff produces no lines, but no crash is the point.
|
||||
assert tc == ""
|
||||
|
||||
|
||||
def test_synthesize_walkthrough_empty_problem_does_not_crash():
|
||||
# An empty `problem` should render as "`a.py` — emoji" with a trailing
|
||||
# space, not raise. Regression guard for splitlines()[0][:80].strip().
|
||||
findings = [{"path": "a.py", "line": 1,
|
||||
"severity": "low", "problem": ""}]
|
||||
w, _, _ = oc._synthesize_summary_fields(findings, "")
|
||||
assert len(w) == 1
|
||||
assert "`a.py`" in w[0]
|
||||
assert "🔵" in w[0] # low severity emoji
|
||||
|
||||
Reference in New Issue
Block a user