feat: opencode review engine + .opencode factory
Replace the single Python model-call reviewer with an opencode agent
factory. A primary 'pragent' agent reads a brief (title/body/diff/config/
prior reviews), inspects the checked-out repo, runs the repo's own linters
via bash, loads review-methodology + findings-schema skills, and emits a
{summary, findings} JSON with per-finding severity/path/line/problem/fix/
suggestion/reference. Dormant security/tests/perf subagent lenses fan out
only on large/risky diffs (lean by default).
pilot/opencode_review.py: fetches the repo archive at the head sha into a
temp workdir, writes .pragent/brief.md, drops the factory, runs
'opencode run --pure --agent pragent --dir <workdir>' headlessly. Isolates
HOME (shared, warmed), strips ANTHROPIC_* env (leaked host vars caused
ProviderModelNotFoundError), stdin=DEVNULL (opencode blocks on stdin),
maps the bare OLLAMA_MODEL to the provider-prefixed ref. No Gitea I/O —
ai_review.review_pr parses + anchors + posts (reuses all v2 logic/tests).
PRAGENT_ENGINE=opencode (default) selects it; =ollama keeps the legacy
direct-call path. Verified end-to-end: posts a real review with a summary
section, inline [CRITICAL]/[HIGH] comments + apply-able suggestions +
reference links, and the sha dedupe marker. 49 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from ai_review import ( # noqa: E402
|
||||
parse_diff_anchors,
|
||||
parse_findings,
|
||||
parse_repo_config,
|
||||
parse_review_output,
|
||||
parse_text_blocks,
|
||||
prior_review_bodies,
|
||||
reviewed_shas,
|
||||
@@ -340,4 +341,90 @@ def test_prior_review_bodies_skips_current_sha():
|
||||
|
||||
def test_format_review_body_has_sha_marker():
|
||||
body = format_review_body("- [high] x:1 — b", "glm-5.2:cloud", "abcdef1234567890")
|
||||
assert "<!-- pragent:sha=abcdef1234567890 -->" in body
|
||||
assert "<!-- pragent:sha=abcdef1234567890 -->" in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_review_output (opencode engine: {summary, findings} + reference)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parse_review_output_summary_and_findings():
|
||||
txt = (
|
||||
"This PR adds an eval helper — risky. See findings.\n\n"
|
||||
"```json\n"
|
||||
'{"summary":"Adds eval() — security risk.","findings":['
|
||||
'{"severity":"critical","path":"src/x.ts","line":4,"problem":"eval on user input",'
|
||||
'"fix":"parse explicitly","suggestion":"const n = Number(s)","reference":"https://owasp.org/x"}'
|
||||
"]}",
|
||||
"\n```",
|
||||
)
|
||||
summary, fs = parse_review_output("".join(txt))
|
||||
assert "eval()" in summary
|
||||
assert len(fs) == 1
|
||||
assert fs[0]["severity"] == "critical"
|
||||
assert fs[0]["reference"] == "https://owasp.org/x"
|
||||
assert fs[0]["suggestion"] == "const n = Number(s)"
|
||||
|
||||
|
||||
def test_parse_review_output_bare_findings_no_summary():
|
||||
txt = '```json\n{"findings":[{"severity":"low","path":"a","line":1,"problem":"p"}]}\n```'
|
||||
summary, fs = parse_review_output(txt)
|
||||
assert summary == ""
|
||||
assert len(fs) == 1
|
||||
assert fs[0]["reference"] == "" # default
|
||||
|
||||
|
||||
def test_parse_review_output_empty_and_bogus():
|
||||
assert parse_review_output("") == ("", [])
|
||||
assert parse_review_output("no json here") == ("", [])
|
||||
assert parse_review_output('{"findings":[]}') == ("", [])
|
||||
|
||||
|
||||
def test_parse_review_output_uses_last_json_block():
|
||||
# Agent emits a stray json-ish block first, then the real one last.
|
||||
txt = (
|
||||
"```json\n{\"findings\":[{\"path\":\"x\",\"line\":1,\"severity\":\"low\"}]}\n```\n"
|
||||
"more prose\n"
|
||||
"```json\n{\"summary\":\"real\",\"findings\":[{\"path\":\"y\",\"line\":2,\"severity\":\"high\"}]}\n```"
|
||||
)
|
||||
summary, fs = parse_review_output(txt)
|
||||
assert summary == "real"
|
||||
assert len(fs) == 1
|
||||
assert fs[0]["path"] == "y"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reference rendering in inline_comment_body + summary_bullets + summary section
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_inline_comment_body_renders_reference():
|
||||
f = {"severity": "high", "path": "a", "line": 1, "problem": "p", "fix": "f",
|
||||
"suggestion": "", "reference": "https://cve.example/X"}
|
||||
body = inline_comment_body(f)
|
||||
assert "📎 ref: https://cve.example/X" in body
|
||||
|
||||
|
||||
def test_inline_comment_body_no_reference_no_ref_line():
|
||||
f = {"severity": "low", "path": "a", "line": 1, "problem": "p", "fix": "",
|
||||
"suggestion": "", "reference": ""}
|
||||
assert "📎 ref" not in inline_comment_body(f)
|
||||
|
||||
|
||||
def test_summary_bullets_renders_reference():
|
||||
fs = [{"severity": "high", "path": "a.py", "line": 7, "problem": "p", "fix": "f",
|
||||
"suggestion": "", "reference": "https://r.example"}]
|
||||
b = summary_bullets(fs)
|
||||
assert "https://r.example" in b
|
||||
assert "`a.py:7`" in b
|
||||
|
||||
|
||||
def test_format_review_body_with_summary_section():
|
||||
body = format_review_body("- [high] x:1 — b", "glm-5.2:cloud", "abcdef1234567890",
|
||||
summary="This PR adds a risky helper.")
|
||||
assert "This PR adds a risky helper." in body
|
||||
assert "- [high] x:1" in body
|
||||
assert "<!-- pragent:sha=abcdef1234567890 -->" in body
|
||||
# summary appears before the findings bullets
|
||||
assert body.index("risky helper") < body.index("[high]")
|
||||
Reference in New Issue
Block a user