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:
@@ -0,0 +1,126 @@
|
||||
"""Unit tests for the opencode engine glue (no network, no opencode run)."""
|
||||
import io
|
||||
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_write_brief_contains_key_sections(tmp_path):
|
||||
brief = oc.write_brief(
|
||||
str(tmp_path),
|
||||
repo="masi/portfolio", index="3", sha="abcdef1234567890",
|
||||
title="Add eval helper", description="Closes #1",
|
||||
diff="diff --git a/x b/x\n+++ b/x\n@@ -1 +1,2 @@\n+eval(input())",
|
||||
config={"focus": ["security"], "instructions": "Flag eval()."},
|
||||
prior_reviews=["🤖 AI Review …\n- [high] old finding"],
|
||||
)
|
||||
assert brief.endswith(".pragent/brief.md")
|
||||
text = open(brief, encoding="utf-8").read()
|
||||
assert "masi/portfolio" in text
|
||||
assert "#3" in text
|
||||
assert "abcdef1234567890" in text
|
||||
assert "Add eval helper" in text
|
||||
assert "Closes #1" in text
|
||||
assert "eval(input())" in text
|
||||
assert "security" in text
|
||||
assert "Flag eval()" in text
|
||||
assert "old finding" in text
|
||||
assert "POST-CHANGE" in text # anchor hint
|
||||
|
||||
|
||||
def test_write_brief_none_config_and_prior(tmp_path):
|
||||
brief = oc.write_brief(
|
||||
str(tmp_path), repo="o/r", index="1", sha="sha1234567",
|
||||
title="t", description="", diff="d", config=None, prior_reviews=None,
|
||||
)
|
||||
text = open(brief, encoding="utf-8").read()
|
||||
assert "_(none)_" in text # both config and prior fall back to none
|
||||
assert "diff" in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _extract_tar_strip_one — strips the single top-level dir
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_tar(top: str) -> bytes:
|
||||
"""Build a tar.gz in memory with one top-level dir `top` containing files."""
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
# dir
|
||||
ti = tarfile.TarInfo(name=f"{top}/")
|
||||
ti.type = tarfile.DIRTYPE
|
||||
tar.addfile(ti)
|
||||
# file src/a.py
|
||||
data = b"print('a')\n"
|
||||
ti = tarfile.TarInfo(name=f"{top}/src/a.py")
|
||||
ti.size = len(data)
|
||||
tar.addfile(ti, io.BytesIO(data))
|
||||
# file README.md
|
||||
data = b"# hi\n"
|
||||
ti = tarfile.TarInfo(name=f"{top}/README.md")
|
||||
ti.size = len(data)
|
||||
tar.addfile(ti, io.BytesIO(data))
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_extract_tar_strips_top_level_dir(tmp_path):
|
||||
blob = _make_tar("repo-deadbeef")
|
||||
oc._extract_tar_strip_one(blob, str(tmp_path))
|
||||
# files sit directly at dest root (prefix stripped)
|
||||
assert os.path.isfile(tmp_path / "README.md")
|
||||
assert os.path.isfile(tmp_path / "src" / "a.py")
|
||||
assert not os.path.isdir(tmp_path / "repo-deadbeef") # top dir gone
|
||||
|
||||
|
||||
def test_extract_tar_no_common_prefix_extracts_as_is(tmp_path):
|
||||
# Two different top-level entries -> no strip.
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
for name, data in (("a.txt", b"A"), ("b.txt", b"B")):
|
||||
ti = tarfile.TarInfo(name=name)
|
||||
ti.size = len(data)
|
||||
tar.addfile(ti, io.BytesIO(data))
|
||||
oc._extract_tar_strip_one(buf.getvalue(), str(tmp_path))
|
||||
assert os.path.isfile(tmp_path / "a.txt")
|
||||
assert os.path.isfile(tmp_path / "b.txt")
|
||||
|
||||
|
||||
def test_extract_tar_skips_parent_traversal(tmp_path):
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
ti = tarfile.TarInfo(name="top/../../escape.txt")
|
||||
data = b"evil"
|
||||
ti.size = len(data)
|
||||
tar.addfile(ti, io.BytesIO(data))
|
||||
ti = tarfile.TarInfo(name="top/ok.txt")
|
||||
data = b"ok"
|
||||
ti.size = len(data)
|
||||
tar.addfile(ti, io.BytesIO(data))
|
||||
oc._extract_tar_strip_one(buf.getvalue(), str(tmp_path))
|
||||
assert os.path.isfile(tmp_path / "ok.txt")
|
||||
assert not os.path.isfile(tmp_path / "escape.txt")
|
||||
assert not os.path.isfile(os.path.join(str(tmp_path), "..", "escape.txt"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# drop_factory — copies opencode.json + .opencode/ from the repo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_drop_factory_copies_config_and_agents(tmp_path):
|
||||
oc.drop_factory(str(tmp_path))
|
||||
assert os.path.isfile(tmp_path / "opencode.json")
|
||||
assert os.path.isfile(tmp_path / ".opencode" / "agents" / "pragent.md")
|
||||
assert os.path.isfile(tmp_path / ".opencode" / "skills" / "findings-schema" / "SKILL.md")
|
||||
Reference in New Issue
Block a user